/*
* ServeStream: A HTTP stream browser/player for Android
* Copyright 2014 William Seemann
*
* 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.
*/
package net.sourceforge.servestream.media;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import javax.net.ssl.HttpsURLConnection;
import net.sourceforge.servestream.bean.UriBean;
import net.sourceforge.servestream.provider.Media;
import net.sourceforge.servestream.transport.HTTP;
import net.sourceforge.servestream.transport.HTTPS;
import net.sourceforge.servestream.transport.TransportFactory;
import net.sourceforge.servestream.utils.Utils;
import android.content.Context;
import android.database.Cursor;
import android.media.MediaPlayer;
import android.net.Uri;
import android.util.Log;
public class DownloadPlayer extends FFmpegMediaPlayer {
private static final String TAG = DownloadPlayer.class.getName();
private long mTotalSizeInBytes = -1;
private URL mUrl = null;
private long mId = -1;
private long mLength = -1;
private File mPartialFile = null;
private File mCompleteFile = null;
private DownloadTask mDownloadTask = null;
private PollingAsyncTask mPollingAsyncTask = null;
public DownloadPlayer() {
super();
}
@Override
public void setDataSource(Context context, long id) throws IOException,
IllegalArgumentException, SecurityException, IllegalStateException {
mId = id;
String path = getUri(context, id);
Uri uri = TransportFactory.getUri(path);
if (uri == null ||
(!uri.getScheme().equals(HTTP.getProtocolName()) &&
!uri.getScheme().equals(HTTPS.getProtocolName()))) {
throw new IllegalArgumentException();
}
UriBean uriBean = TransportFactory.getTransport(uri.getScheme()).createUri(uri);
if (uriBean == null) {
throw new IllegalArgumentException();
}
mUrl = uriBean.getURL();
}
@Override
public void prepareAsync() throws IllegalStateException {
download();
}
@Override
public void stop() {
super.stop();
cancelDownload();
}
@Override
public void seekTo(int msec) throws IllegalStateException {
if (isCompleteFileAvailable()) {
super.seekTo(msec);
}
}
@Override
public int getDuration() {
if (isCompleteFileAvailable()) {
return super.getDuration();
} else {
return 0;
}
}
@Override
public void release() {
super.release();
cancelDownload();
Utils.deleteAllFiles();
}
@Override
public void reset() {
super.reset();
cancelDownload();
Utils.deleteAllFiles();
}
private void download() {
mTotalSizeInBytes = -1;
mLength = -1;
mPartialFile = new File(Utils.getDownloadDirectory(), "mediafile" + mId + ".partial.dat");
mCompleteFile = new File(Utils.getDownloadDirectory(), "mediafile" + mId + ".complete.dat");
Utils.deleteFile(mPartialFile);
Utils.deleteFile(mCompleteFile);
Log.v(TAG, "=============> " + mPartialFile.toString());
mDownloadTask = new DownloadTask(mUrl, mPartialFile, mCompleteFile);
mDownloadTask.execute();
mPollingAsyncTask = new PollingAsyncTask();
mPollingAsyncTask.execute();
}
public void cancelDownload() {
if (mDownloadTask != null) {
DownloadTask downloadTask = mDownloadTask;
downloadTask.cancel();
mDownloadTask = null;
}
}
public void cancelPollingTask() {
if (mPollingAsyncTask != null) {
PollingAsyncTask pollingAsyncTask = mPollingAsyncTask;
pollingAsyncTask.cancel();
mPollingAsyncTask = null;
}
}
public File getCompleteFile() {
if (mCompleteFile != null && mCompleteFile.exists()) {
return mCompleteFile;
}
return null;
}
public long getCompleteFileDuration() {
long duration = 0;
MediaPlayer mediaPlayer = new MediaPlayer();
try {
mediaPlayer.setDataSource(getCompleteFile().toString());
mediaPlayer.prepare();
duration = mediaPlayer.getDuration();
mediaPlayer.release();
} catch (Exception e) {
}
return duration;
}
public File getPartialFile() {
return mPartialFile;
}
public synchronized boolean isCompleteFileAvailable() {
if (mCompleteFile != null && mCompleteFile.exists()) {
return true;
}
return false;
}
public synchronized double getPercentDownloaded() {
if (mCompleteFile != null && mCompleteFile.exists()) {
return 1.0;
}
return (double) mPartialFile.length() / (double) mTotalSizeInBytes;
}
public synchronized boolean isDownloadCancelled() {
return mDownloadTask != null && mDownloadTask.isCancelled();
}
public void delete() {
cancelDownload();
cancelPollingTask();
Utils.deleteFile(mPartialFile);
Utils.deleteFile(mCompleteFile);
}
/**
* @return the length
*/
public long getLength() {
long length = -1;
if (isCompleteFileAvailable()) {
if (mLength == -1) {
mLength = getCompleteFileDuration();
}
length = mLength;
}
return length;
}
private class DownloadTask implements Runnable {
private boolean mIsCancelled;
private URL mUrl = null;
private File mPartialFile = null;
private File mCompleteFile = null;
public DownloadTask(URL url, File partialFile, File completeFile) {
mIsCancelled = false;
mUrl = url;
mPartialFile = partialFile;
mCompleteFile = completeFile;
}
@Override
public void run() {
HttpURLConnection conn = null;
BufferedInputStream in = null;
FileOutputStream out = null;
boolean appendToFile = false;
byte[] buffer = new byte[1024 * 16];
//long count = 0;
Log.v(TAG, "starting download task");
while (!mCompleteFile.exists() && !isCancelled()) {
try {
conn = determineRange(mUrl, mPartialFile.length());
if (conn.getResponseCode() == HttpURLConnection.HTTP_PARTIAL) {
appendToFile = true;
} else {
mTotalSizeInBytes = conn.getContentLength();
}
in = new BufferedInputStream(conn.getInputStream());
out = new FileOutputStream(mPartialFile, appendToFile);
int i;
while (((i = in.read(buffer)) != -1) && !isCancelled()) {
out.write(buffer, 0, i);
//count += i;
}
out.flush();
out.close();
Utils.copyFile(mPartialFile, mCompleteFile);
Log.v(TAG, "download task is complete");
if (mOnInfoListener != null) {
mOnInfoListener.onInfo(DownloadPlayer.this, AbstractMediaPlayer.MEDIA_INFO_METADATA_UPDATE, 0);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
Utils.closeInputStream(in);
Utils.closeHttpConnection(conn);
Utils.closeOutputStream(out);
}
}
}
private HttpURLConnection determineRange(URL url, long bytesProcessed) {
HttpURLConnection conn = null;
try {
if (url.getProtocol().equalsIgnoreCase("http")) {
conn = (HttpURLConnection) url.openConnection();
} else if (url.getProtocol().equalsIgnoreCase("https")) {
conn = (HttpsURLConnection) url.openConnection();
}
if (conn == null) {
return null;
}
conn.setRequestProperty("User-Agent", "ServeStream");
conn.setConnectTimeout(6000);
conn.setReadTimeout(6000);
conn.setRequestProperty("Range", "bytes=" + bytesProcessed + "-");
conn.setRequestMethod("GET");
conn.connect();
} catch (IOException e) {
e.printStackTrace();
}
return conn;
}
private synchronized boolean isCancelled() {
return mIsCancelled;
}
public synchronized void cancel() {
mIsCancelled = true;
}
public synchronized void execute() {
new Thread(this, "").start();
}
}
private class PollingAsyncTask implements Runnable {
private boolean mIsCancelled;
//int INITIAL_BUFFER = Math.max(100000, 160 * 1024 / 8 * 5);
int INITIAL_BUFFER = 81920;
public PollingAsyncTask() {
mIsCancelled = false;
}
@Override
public void run() {
Log.v(TAG, "polling task started");
while (!bufferingComplete() && !isCancelled()) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Log.v(TAG, "setDataSource called");
try {
DownloadPlayer.super.setDataSource(getPartialFile().getPath());
DownloadPlayer.super.prepareAsync();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (SecurityException e) {
e.printStackTrace();
} catch (IllegalStateException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
private boolean bufferingComplete() {
return getPartialFile().length() >= INITIAL_BUFFER;
}
private synchronized boolean isCancelled() {
return mIsCancelled;
}
public synchronized void cancel() {
mIsCancelled = true;
}
public synchronized void execute() {
new Thread(this, "").start();
}
}
private String getUri(Context context, long id) {
String uri = null;
// Form an array specifying which columns to return.
String [] projection = new String [] { Media.MediaColumns.URI };
// Get the base URI for the Media Files table in the Media content provider.
Uri mediaFile = Media.MediaColumns.CONTENT_URI;
// Make the query.
Cursor cursor = context.getContentResolver().query(mediaFile,
projection,
Media.MediaColumns._ID + "= ? ",
new String [] { String.valueOf(id) },
null);
if (cursor.moveToFirst()) {
int uriColumn = cursor.getColumnIndex(Media.MediaColumns.URI);
uri = cursor.getString(uriColumn);
}
cursor.close();
return uri;
}
}