aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--subsonic-android/src/github/daneren2005/dsub/service/DownloadServiceImpl.java106
-rw-r--r--subsonic-android/src/github/daneren2005/dsub/service/StreamProxy.java246
2 files changed, 309 insertions, 43 deletions
diff --git a/subsonic-android/src/github/daneren2005/dsub/service/DownloadServiceImpl.java b/subsonic-android/src/github/daneren2005/dsub/service/DownloadServiceImpl.java
index 759757cb..4e7bbbb0 100644
--- a/subsonic-android/src/github/daneren2005/dsub/service/DownloadServiceImpl.java
+++ b/subsonic-android/src/github/daneren2005/dsub/service/DownloadServiceImpl.java
@@ -61,6 +61,7 @@ import android.os.IBinder;
import android.os.PowerManager;
import android.util.Log;
import github.daneren2005.dsub.activity.SubsonicTabActivity;
+import java.net.URLEncoder;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@@ -120,6 +121,7 @@ public class DownloadServiceImpl extends Service implements DownloadService {
private boolean showVisualization;
private boolean jukeboxEnabled;
private ScheduledExecutorService executorService;
+ private StreamProxy proxy;
private Timer sleepTimer;
private int timerDuration;
@@ -495,7 +497,6 @@ public class DownloadServiceImpl extends Service implements DownloadService {
if (currentPlaying != null) {
Util.broadcastNewTrackInfo(this, currentPlaying.getSong());
- currentPlaying.setPlaying(true);
mRemoteControl.updateMetadata(this, currentPlaying.getSong());
} else {
Util.broadcastNewTrackInfo(this, null);
@@ -525,6 +526,7 @@ public class DownloadServiceImpl extends Service implements DownloadService {
}
}
+ nextSetup = false;
if(index < size() && index != -1) {
nextPlaying = downloadList.get(index);
nextPlayingTask = new CheckCompletionTask(nextPlaying);
@@ -624,6 +626,12 @@ public class DownloadServiceImpl extends Service implements DownloadService {
setPlayerState(PlayerState.STARTED);
setupHandlers(currentPlaying, false);
setNextPlaying();
+
+ // Proxy should not be being used here since the next player was already setup to play
+ if(proxy != null) {
+ proxy.stop();
+ proxy = null;
+ }
}
/** Plays or resumes the playback, depending on the current player state. */
@@ -922,58 +930,69 @@ public class DownloadServiceImpl extends Service implements DownloadService {
private synchronized void doPlay(final DownloadFile downloadFile, final int position, final boolean start) {
try {
+ downloadFile.setPlaying(true);
final File file = downloadFile.isCompleteFileAvailable() ? downloadFile.getCompleteFile() : downloadFile.getPartialFile();
isPartial = file.equals(downloadFile.getPartialFile());
downloadFile.updateModificationDate();
- if(playerState == PlayerState.PREPARED) {
- if (start) {
- mediaPlayer.start();
- setPlayerState(STARTED);
- } else {
- setPlayerState(PAUSED);
+ mediaPlayer.setOnCompletionListener(null);
+ mediaPlayer.reset();
+ setPlayerState(IDLE);
+ mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
+ String dataSource = file.getPath();
+ if(isPartial) {
+ if (proxy == null) {
+ proxy = new StreamProxy(this);
+ proxy.start();
}
- } else {
- mediaPlayer.setOnCompletionListener(null);
- mediaPlayer.reset();
- setPlayerState(IDLE);
- mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
- mediaPlayer.setDataSource(file.getPath());
- setPlayerState(PREPARING);
-
- mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
- public void onPrepared(MediaPlayer mediaPlayer) {
- try {
- setPlayerState(PREPARED);
-
- synchronized (DownloadServiceImpl.this) {
- if (position != 0) {
- Log.i(TAG, "Restarting player from position " + position);
- mediaPlayer.seekTo(position);
- }
- cachedPosition = position;
-
- if (start) {
- mediaPlayer.start();
- setPlayerState(STARTED);
- } else {
- setPlayerState(PAUSED);
- }
+ dataSource = String.format("http://127.0.0.1:%d/%s", proxy.getPort(), URLEncoder.encode(dataSource, Constants.UTF_8));
+ Log.i(TAG, "Data Source: " + dataSource);
+ } else if(proxy != null) {
+ proxy.stop();
+ proxy = null;
+ }
+ mediaPlayer.setDataSource(dataSource);
+ setPlayerState(PREPARING);
+
+ mediaPlayer.setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() {
+ public void onBufferingUpdate(MediaPlayer mp, int percent) {
+ Log.i(TAG, "Buffered " + percent + "%");
+ if(percent == 100) {
+ mediaPlayer.setOnBufferingUpdateListener(null);
+ }
+ }
+ });
+
+ mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
+ public void onPrepared(MediaPlayer mediaPlayer) {
+ try {
+ setPlayerState(PREPARED);
+
+ synchronized (DownloadServiceImpl.this) {
+ if (position != 0) {
+ Log.i(TAG, "Restarting player from position " + position);
+ mediaPlayer.seekTo(position);
}
+ cachedPosition = position;
- lifecycleSupport.serializeDownloadQueue();
- } catch (Exception x) {
- handleError(x);
+ if (start) {
+ mediaPlayer.start();
+ setPlayerState(STARTED);
+ } else {
+ setPlayerState(PAUSED);
+ }
}
+
+ lifecycleSupport.serializeDownloadQueue();
+ } catch (Exception x) {
+ handleError(x);
}
- });
- }
+ }
+ });
setupHandlers(downloadFile, isPartial);
- if(playerState == PREPARING) {
- mediaPlayer.prepareAsync();
- }
+ mediaPlayer.prepareAsync();
} catch (Exception x) {
handleError(x);
}
@@ -993,8 +1012,7 @@ public class DownloadServiceImpl extends Service implements DownloadService {
public void onPrepared(MediaPlayer mediaPlayer) {
try {
setNextPlayerState(PREPARED);
- // TODO: Whenever the completing early issue is fixed, remove !isPartial to get gapless playback on streams as well
- if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && playerState == PlayerState.STARTED && !isPartial) {
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && playerState == PlayerState.STARTED) {
DownloadServiceImpl.this.mediaPlayer.setNextMediaPlayer(nextMediaPlayer);
nextSetup = true;
}
@@ -1362,6 +1380,8 @@ public class DownloadServiceImpl extends Service implements DownloadService {
return;
}
+ // Do an initial sleep so this prepare can't compete with main prepare
+ Util.sleepQuietly(5000L);
while (!bufferComplete()) {
Util.sleepQuietly(5000L);
if (isCancelled()) {
diff --git a/subsonic-android/src/github/daneren2005/dsub/service/StreamProxy.java b/subsonic-android/src/github/daneren2005/dsub/service/StreamProxy.java
new file mode 100644
index 00000000..1b422d5a
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/dsub/service/StreamProxy.java
@@ -0,0 +1,246 @@
+package github.daneren2005.dsub.service;
+
+import java.io.BufferedOutputStream;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.InetAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.SocketException;
+import java.net.SocketTimeoutException;
+import java.net.URLDecoder;
+import java.net.UnknownHostException;
+import java.util.StringTokenizer;
+
+import org.apache.http.HttpRequest;
+import org.apache.http.message.BasicHttpRequest;
+
+import android.os.AsyncTask;
+import android.os.Looper;
+import android.util.Log;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.util.Constants;
+
+public class StreamProxy implements Runnable {
+ private static final String TAG = StreamProxy.class.getSimpleName();
+
+ private Thread thread;
+ private boolean isRunning;
+ private ServerSocket socket;
+ private int port;
+ private DownloadService downloadService;
+
+ public StreamProxy(DownloadService downloadService) {
+
+ // Create listening socket
+ try {
+ socket = new ServerSocket(0, 0, InetAddress.getByAddress(new byte[] { 127, 0, 0, 1 }));
+ socket.setSoTimeout(5000);
+ port = socket.getLocalPort();
+ this.downloadService = downloadService;
+ } catch (UnknownHostException e) { // impossible
+ } catch (IOException e) {
+ Log.e(TAG, "IOException initializing server", e);
+ }
+ }
+
+ public int getPort() {
+ return port;
+ }
+
+ public void start() {
+ thread = new Thread(this);
+ thread.start();
+ }
+
+ public void stop() {
+ isRunning = false;
+ thread.interrupt();
+ try {
+ thread.join(5000);
+ } catch (InterruptedException e) {
+ Log.e(TAG, "Exception stopping server", e);
+ }
+ }
+
+ @Override
+ public void run() {
+ Looper.prepare();
+ isRunning = true;
+ while (isRunning) {
+ try {
+ Socket client = socket.accept();
+ if (client == null) {
+ continue;
+ }
+ Log.d(TAG, "client connected");
+
+ StreamToMediaPlayerTask task = new StreamToMediaPlayerTask(client);
+ if (task.processRequest()) {
+ task.execute();
+ }
+
+ } catch (SocketTimeoutException e) {
+ // Do nothing
+ } catch (IOException e) {
+ Log.e(TAG, "Error connecting to client", e);
+ }
+ }
+ Log.d(TAG, "Proxy interrupted. Shutting down.");
+ }
+
+ private class StreamToMediaPlayerTask extends AsyncTask<String, Void, Integer> {
+
+ String localPath;
+ Socket client;
+ int cbSkip;
+
+ public StreamToMediaPlayerTask(Socket client) {
+ this.client = client;
+ }
+
+ private HttpRequest readRequest() {
+ HttpRequest request = null;
+ InputStream is;
+ String firstLine;
+ try {
+ is = client.getInputStream();
+ BufferedReader reader = new BufferedReader(new InputStreamReader(is), 8192);
+ firstLine = reader.readLine();
+ } catch (IOException e) {
+ Log.e(TAG, "Error parsing request", e);
+ return request;
+ }
+
+ if (firstLine == null) {
+ Log.i(TAG, "Proxy client closed connection without a request.");
+ return request;
+ }
+
+ StringTokenizer st = new StringTokenizer(firstLine);
+ String method = st.nextToken();
+ String uri = st.nextToken();
+ String realUri = uri.substring(1);
+ Log.i(TAG, realUri);
+ request = new BasicHttpRequest(method, realUri);
+ return request;
+ }
+
+ public boolean processRequest() {
+ HttpRequest request = readRequest();
+ if (request == null) {
+ return false;
+ }
+
+ // Read HTTP headers
+ Log.i(TAG, "Processing request");
+
+ try {
+ localPath = URLDecoder.decode(request.getRequestLine().getUri(), Constants.UTF_8);
+ } catch (UnsupportedEncodingException e) {
+ Log.e(TAG, "Unsupported encoding", e);
+ return false;
+ }
+
+ File file = new File(localPath);
+ if (!file.exists()) {
+ Log.e(TAG, "File " + localPath + " does not exist");
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ protected Integer doInBackground(String... params) {
+ DownloadFile downloadFile = downloadService.getCurrentPlaying();
+ MusicDirectory.Entry song = downloadFile.getSong();
+ long fileSize = downloadFile.getBitRate() * song.getDuration() * 1000 / 8;
+ Log.i(TAG, "Streaming fileSize: " + fileSize);
+
+ // Create HTTP header
+ String headers = "HTTP/1.0 200 OK\r\n";
+ headers += "Content-Type: " + "application/octet-stream" + "\r\n";
+
+ headers += "Connection: close\r\n";
+ headers += "\r\n";
+
+ long cbToSend = fileSize - cbSkip;
+ OutputStream output = null;
+ byte[] buff = new byte[64 * 1024];
+ try {
+ output = new BufferedOutputStream(client.getOutputStream(), 32*1024);
+ output.write(headers.getBytes());
+
+ if(!downloadFile.isWorkDone()) {
+ // Loop as long as there's stuff to send
+ while (isRunning && !client.isClosed()) {
+
+ // See if there's more to send
+ File file = new File(localPath);
+ int cbSentThisBatch = 0;
+ if (file.exists()) {
+ FileInputStream input = new FileInputStream(file);
+ input.skip(cbSkip);
+ int cbToSendThisBatch = input.available();
+ while (cbToSendThisBatch > 0) {
+ int cbToRead = Math.min(cbToSendThisBatch, buff.length);
+ int cbRead = input.read(buff, 0, cbToRead);
+ if (cbRead == -1) {
+ break;
+ }
+ cbToSendThisBatch -= cbRead;
+ cbToSend -= cbRead;
+ output.write(buff, 0, cbRead);
+ output.flush();
+ cbSkip += cbRead;
+ cbSentThisBatch += cbRead;
+ }
+ input.close();
+ }
+
+ // Done regardless of whether or not it thinks it is
+ if(downloadFile.isWorkDone() && cbSkip >= file.length()) {
+ break;
+ }
+
+ // If we did nothing this batch, block for a second
+ if (cbSentThisBatch == 0) {
+ Log.d(TAG, "Blocking until more data appears (" + cbToSend + ")");
+ Thread.sleep(1000);
+ }
+ }
+ } else {
+ Log.w(TAG, "Requesting data for completely downloaded file");
+ }
+ }
+ catch (SocketException socketException) {
+ Log.e(TAG, "SocketException() thrown, proxy client has probably closed. This can exit harmlessly");
+ }
+ catch (Exception e) {
+ Log.e(TAG, "Exception thrown from streaming task:");
+ Log.e(TAG, e.getClass().getName() + " : " + e.getLocalizedMessage());
+ }
+
+ // Cleanup
+ try {
+ if (output != null) {
+ output.close();
+ }
+ client.close();
+ }
+ catch (IOException e) {
+ Log.e(TAG, "IOException while cleaning up streaming task:");
+ Log.e(TAG, e.getClass().getName() + " : " + e.getLocalizedMessage());
+ }
+
+ return 1;
+ }
+ }
+} \ No newline at end of file