diff options
-rw-r--r-- | subsonic-android/src/github/daneren2005/dsub/service/DownloadServiceImpl.java | 106 | ||||
-rw-r--r-- | subsonic-android/src/github/daneren2005/dsub/service/StreamProxy.java | 246 |
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 |