Experimental changes that use AndroidVideoCache library as a local

streaming proxy -- it, in theory, controls caching and retries better
than OkHttp.
This commit is contained in:
casey langen 2017-05-31 00:45:47 -07:00
parent a5b7ab5107
commit ab11376356
8 changed files with 179 additions and 143 deletions

View File

@ -41,13 +41,16 @@ dependencies {
compile(name:'android-taskrunner-0.5', ext:'aar')
compile(name:'videocache-2.8.0-pre', ext:'aar')
compile 'org.slf4j:slf4j-android:1.7.21'
compile 'com.neovisionaries:nv-websocket-client:1.31'
compile 'com.squareup.okhttp3:okhttp:3.6.0'
compile 'com.github.bumptech.glide:glide:3.7.0'
compile 'io.reactivex.rxjava2:rxjava:2.0.9'
compile 'io.reactivex.rxjava2:rxandroid:2.0.1'
compile 'com.google.android.exoplayer:exoplayer:r2.4.0'
compile 'com.google.android.exoplayer:extension-okhttp:r2.4.0'
compile 'com.google.android.exoplayer:exoplayer:r2.4.1'
compile 'com.google.android.exoplayer:extension-okhttp:r2.4.1'
compile 'com.github.pluscubed:recycler-fast-scroll:0.3.2@aar'
compile 'com.android.support:appcompat-v7:25.3.1'

Binary file not shown.

View File

@ -1,12 +1,17 @@
package io.casey.musikcube.remote;
import io.casey.musikcube.remote.playback.StreamProxy;
import io.casey.musikcube.remote.util.NetworkUtil;
public class Application extends android.app.Application {
private static Application instance;
@Override
public void onCreate() {
super.onCreate();
instance = this;
super.onCreate();
NetworkUtil.init();
StreamProxy.init(this);
}
public static Application getInstance() {

View File

@ -1,9 +1,7 @@
package io.casey.musikcube.remote.playback;
import android.content.Context;
import android.content.SharedPreferences;
import android.net.Uri;
import android.util.Base64;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
@ -11,7 +9,6 @@ import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSourceFactory;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
@ -28,107 +25,26 @@ import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.util.Util;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import io.casey.musikcube.remote.Application;
import io.casey.musikcube.remote.util.NetworkUtil;
import io.casey.musikcube.remote.util.Preconditions;
import io.casey.musikcube.remote.websocket.Prefs;
import okhttp3.Cache;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
public class ExoPlayerWrapper extends PlayerWrapper {
private static OkHttpClient audioStreamHttpClient = null;
private static final long BYTES_PER_MEGABYTE = 1048576L;
private static final long BYTES_PER_GIGABYTE = 1073741824L;
private static final Map<Integer, Long> CACHE_SETTING_TO_BYTES;
static {
CACHE_SETTING_TO_BYTES = new HashMap<>();
CACHE_SETTING_TO_BYTES.put(0, BYTES_PER_MEGABYTE * 32);
CACHE_SETTING_TO_BYTES.put(1, BYTES_PER_GIGABYTE / 2);
CACHE_SETTING_TO_BYTES.put(2, BYTES_PER_GIGABYTE);
CACHE_SETTING_TO_BYTES.put(3, BYTES_PER_GIGABYTE * 2);
CACHE_SETTING_TO_BYTES.put(4, BYTES_PER_GIGABYTE * 3);
CACHE_SETTING_TO_BYTES.put(5, BYTES_PER_GIGABYTE * 4);
}
private DefaultBandwidthMeter bandwidth;
private DataSource.Factory datasources;
private ExtractorsFactory extractors;
private MediaSource source;
private SimpleExoPlayer player;
private boolean prefetch;
private Context context;
private SharedPreferences prefs;
public static void invalidateSettings() {
synchronized (ExoPlayerWrapper.class) {
audioStreamHttpClient = null;
}
}
public ExoPlayerWrapper() {
this.context = Application.getInstance();
this.prefs = context.getSharedPreferences(Prefs.NAME, Context.MODE_PRIVATE);
this.bandwidth = new DefaultBandwidthMeter();
final DefaultBandwidthMeter bandwidth = new DefaultBandwidthMeter();
final TrackSelection.Factory trackFactory = new AdaptiveTrackSelection.Factory(bandwidth);
final TrackSelector trackSelector = new DefaultTrackSelector(trackFactory);
this.player = ExoPlayerFactory.newSimpleInstance(this.context, trackSelector);
this.extractors = new DefaultExtractorsFactory();
this.player.addListener(eventListener);
}
private void initHttpClient(final String uri) {
final Context context = Application.getInstance();
synchronized (ExoPlayerWrapper.class) {
if (audioStreamHttpClient == null) {
final SharedPreferences prefs = ExoPlayerWrapper.this.prefs;
final File path = new File(context.getExternalCacheDir(), "audio");
int diskCacheIndex = this.prefs.getInt(
Prefs.Key.DISK_CACHE_SIZE_INDEX, Prefs.Default.DISK_CACHE_SIZE_INDEX);
if (diskCacheIndex < 0 || diskCacheIndex > CACHE_SETTING_TO_BYTES.size()) {
diskCacheIndex = 0;
}
final OkHttpClient.Builder builder = new OkHttpClient.Builder()
.cache(new Cache(path, CACHE_SETTING_TO_BYTES.get(diskCacheIndex)))
.addInterceptor((chain) -> {
Request request = chain.request();
final String userPass = "default:" + prefs.getString(Prefs.Key.PASSWORD, Prefs.Default.PASSWORD);
final String encoded = Base64.encodeToString(userPass.getBytes(), Base64.NO_WRAP);
request = request.newBuilder().addHeader("Authorization", "Basic " + encoded).build();
return chain.proceed(request);
});
if (this.prefs.getBoolean(Prefs.Key.CERT_VALIDATION_DISABLED, Prefs.Default.CERT_VALIDATION_DISABLED)) {
NetworkUtil.disableCertificateValidation(builder);
}
audioStreamHttpClient = builder.build();
}
}
if (uri.startsWith("http")) {
this.datasources = new OkHttpDataSourceFactory(
audioStreamHttpClient,
Util.getUserAgent(context, "musikdroid"),
bandwidth);
}
else {
this.datasources = new DefaultDataSourceFactory(
context, Util.getUserAgent(context, "musikdroid"));
}
this.datasources = new DefaultDataSourceFactory(context, Util.getUserAgent(context, "musikdroid"));
}
@Override
@ -136,8 +52,8 @@ public class ExoPlayerWrapper extends PlayerWrapper {
Preconditions.throwIfNotOnMainThread();
if (!dead()) {
initHttpClient(uri);
this.source = new ExtractorMediaSource(Uri.parse(uri), datasources, extractors, null, null);
final String proxyUri = StreamProxy.getProxyUrl(context, uri);
this.source = new ExtractorMediaSource(Uri.parse(proxyUri), datasources, extractors, null, null);
this.player.setPlayWhenReady(true);
this.player.prepare(this.source);
addActivePlayer(this);
@ -150,9 +66,9 @@ public class ExoPlayerWrapper extends PlayerWrapper {
Preconditions.throwIfNotOnMainThread();
if (!dead()) {
initHttpClient(uri);
this.prefetch = true;
this.source = new ExtractorMediaSource(Uri.parse(uri), datasources, extractors, null, null);
final String proxyUri = StreamProxy.getProxyUrl(context, uri);
this.source = new ExtractorMediaSource(Uri.parse(proxyUri), datasources, extractors, null, null);
this.player.setPlayWhenReady(false);
this.player.prepare(this.source);
addActivePlayer(this);
@ -198,7 +114,9 @@ public class ExoPlayerWrapper extends PlayerWrapper {
Preconditions.throwIfNotOnMainThread();
if (this.player.getPlaybackState() != ExoPlayer.STATE_IDLE) {
this.player.seekTo(millis);
if (this.player.isCurrentWindowSeekable()) {
this.player.seekTo(millis);
}
}
}
@ -258,7 +176,6 @@ public class ExoPlayerWrapper extends PlayerWrapper {
private ExoPlayer.EventListener eventListener = new ExoPlayer.EventListener() {
@Override
public void onTimelineChanged(Timeline timeline, Object manifest) {
}
@Override
@ -303,26 +220,6 @@ public class ExoPlayerWrapper extends PlayerWrapper {
public void onPlayerError(ExoPlaybackException error) {
Preconditions.throwIfNotOnMainThread();
/* if we're transcoding the size of the response will be inexact, so the player
will try to pick up the last few bytes and be left with an HTTP 416. if that happens,
and we're towards the end of the track, just move to the next one */
if (error.getCause() instanceof HttpDataSource.InvalidResponseCodeException) {
final HttpDataSource.InvalidResponseCodeException ex
= (HttpDataSource.InvalidResponseCodeException) error.getCause();
if (ex.responseCode == 416) {
if (Math.abs(getDuration() - getPosition()) < 2000) {
setState(State.Finished);
dispose();
return;
}
else {
player.setPlayWhenReady(false);
setPosition(0);
}
}
}
switch (getState()) {
case Preparing:
case Prepared:

View File

@ -0,0 +1,85 @@
package io.casey.musikcube.remote.playback;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Base64;
import com.danikula.videocache.HttpProxyCacheServer;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
import io.casey.musikcube.remote.util.NetworkUtil;
import io.casey.musikcube.remote.websocket.Prefs;
public class StreamProxy {
private static final long BYTES_PER_MEGABYTE = 1048576L;
private static final long BYTES_PER_GIGABYTE = 1073741824L;
private static final Map<Integer, Long> CACHE_SETTING_TO_BYTES;
static {
CACHE_SETTING_TO_BYTES = new HashMap<>();
CACHE_SETTING_TO_BYTES.put(0, BYTES_PER_MEGABYTE * 32);
CACHE_SETTING_TO_BYTES.put(1, BYTES_PER_GIGABYTE / 2);
CACHE_SETTING_TO_BYTES.put(2, BYTES_PER_GIGABYTE);
CACHE_SETTING_TO_BYTES.put(3, BYTES_PER_GIGABYTE * 2);
CACHE_SETTING_TO_BYTES.put(4, BYTES_PER_GIGABYTE * 3);
CACHE_SETTING_TO_BYTES.put(5, BYTES_PER_GIGABYTE * 4);
}
private static StreamProxy INSTANCE;
private HttpProxyCacheServer proxy;
private SharedPreferences prefs;
private StreamProxy(final Context context) {
prefs = context.getSharedPreferences(Prefs.NAME, Context.MODE_PRIVATE);
if (this.prefs.getBoolean(Prefs.Key.CERT_VALIDATION_DISABLED, Prefs.Default.CERT_VALIDATION_DISABLED)) {
NetworkUtil.disableCertificateValidation();
}
else {
NetworkUtil.enableCertificateValidation();
}
int diskCacheIndex = this.prefs.getInt(
Prefs.Key.DISK_CACHE_SIZE_INDEX, Prefs.Default.DISK_CACHE_SIZE_INDEX);
if (diskCacheIndex < 0 || diskCacheIndex > CACHE_SETTING_TO_BYTES.size()) {
diskCacheIndex = 0;
}
final File cachePath = new File(context.getExternalCacheDir(), "audio");
proxy = new HttpProxyCacheServer.Builder(context.getApplicationContext())
.cacheDirectory(cachePath)
.maxCacheSize(CACHE_SETTING_TO_BYTES.get(diskCacheIndex))
.headerInjector((url) -> {
Map<String, String> headers = new HashMap<>();
final String userPass = "default:" + prefs.getString(Prefs.Key.PASSWORD, Prefs.Default.PASSWORD);
final String encoded = Base64.encodeToString(userPass.getBytes(), Base64.NO_WRAP);
headers.put("Authorization", "Basic " + encoded);
return headers;
}).build();
}
public static synchronized void init(final Context context) {
if (INSTANCE == null) {
INSTANCE = new StreamProxy(context.getApplicationContext());
}
}
public static synchronized String getProxyUrl(final Context context, final String url) {
init(context);
return INSTANCE.proxy.getProxyUrl(url);
}
public static synchronized void reload() {
if (INSTANCE != null) {
INSTANCE.proxy.shutdown();
INSTANCE = null;
}
}
}

View File

@ -22,9 +22,9 @@ import android.widget.Spinner;
import java.util.Locale;
import io.casey.musikcube.remote.R;
import io.casey.musikcube.remote.playback.ExoPlayerWrapper;
import io.casey.musikcube.remote.playback.MediaPlayerWrapper;
import io.casey.musikcube.remote.playback.PlaybackServiceFactory;
import io.casey.musikcube.remote.playback.StreamProxy;
import io.casey.musikcube.remote.ui.util.Views;
import io.casey.musikcube.remote.websocket.Prefs;
import io.casey.musikcube.remote.websocket.WebSocketService;
@ -211,7 +211,7 @@ public class SettingsActivity extends AppCompatActivity {
PlaybackServiceFactory.streaming(this).stop();
}
ExoPlayerWrapper.invalidateSettings();
StreamProxy.reload();
WebSocketService.getInstance(this).disconnect();
finish();

View File

@ -4,6 +4,8 @@ import com.neovisionaries.ws.client.WebSocketFactory;
import java.security.cert.CertificateException;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
@ -13,11 +15,31 @@ import okhttp3.OkHttpClient;
public class NetworkUtil {
private static SSLContext sslContext;
private static SSLSocketFactory sslSocketFactory;
private static SSLSocketFactory insecureSslSocketFactory;
private static SSLSocketFactory originalHttpsUrlConnectionSocketFactory;
private static HostnameVerifier originalHttpsUrlConnectionHostnameVerifier;
public synchronized static void init() {
if (originalHttpsUrlConnectionHostnameVerifier == null) {
originalHttpsUrlConnectionSocketFactory = HttpsURLConnection.getDefaultSSLSocketFactory();
originalHttpsUrlConnectionHostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier();
}
if (sslContext == null) {
try {
sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
insecureSslSocketFactory = sslContext.getSocketFactory();
}
catch (Exception ex) {
throw new RuntimeException(ex);
}
}
}
public static void disableCertificateValidation(final OkHttpClient.Builder okHttpClient) {
init();
okHttpClient.sslSocketFactory(sslSocketFactory, (X509TrustManager)trustAllCerts[0]);
okHttpClient.sslSocketFactory(insecureSslSocketFactory, (X509TrustManager)trustAllCerts[0]);
okHttpClient.hostnameVerifier((hostname, session) -> true);
}
@ -25,14 +47,23 @@ public class NetworkUtil {
socketFactory.setSSLContext(sslContext);
}
private synchronized static void init() {
public static void disableCertificateValidation() {
try {
sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
sslSocketFactory = sslContext.getSocketFactory();
HttpsURLConnection.setDefaultSSLSocketFactory(insecureSslSocketFactory);
HttpsURLConnection.setDefaultHostnameVerifier((url, session) -> true);
}
catch (Exception ex) {
throw new RuntimeException (ex);
catch (Exception e) {
throw new RuntimeException("should never happen");
}
}
public static void enableCertificateValidation() {
try {
HttpsURLConnection.setDefaultSSLSocketFactory(originalHttpsUrlConnectionSocketFactory);
HttpsURLConnection.setDefaultHostnameVerifier(originalHttpsUrlConnectionHostnameVerifier);
}
catch (Exception e) {
throw new RuntimeException("should never happen");
}
}

View File

@ -57,6 +57,8 @@
#include <vector>
#define HTTP_416_DISABLED true
using namespace musik::core::sdk;
std::unordered_map<std::string, std::string> CONTENT_TYPE_MAP = {
@ -357,25 +359,35 @@ int HttpServer::HandleRequest(
if (range->from != 0 || range->to != range->total - 1) {
delete range;
if (file) {
file->Destroy();
file = nullptr;
}
if (false && server->context.prefs->GetBool(
prefs::transcoder_synchronous_fallback.c_str(),
defaults::transcoder_synchronous_fallback))
{
/* if we're allowed, fall back to synchronous transcoding. we'll block
here until the entire file has been converted and cached */
file = Transcoder::TranscodeAndWait(server->context, filename, bitrate);
range = parseRange(file, rangeVal);
if (HTTP_416_DISABLED) {
/* lots of clients don't seem to be to deal with 416 properly;
instead, ignore the range header and return the whole file,
and a 200 (not 206) */
if (file) {
range = parseRange(file, nullptr);
}
}
else {
/* otherwise fail with a "range not satisfiable" status */
status = 416;
char empty[1];
response = MHD_create_response_from_buffer(0, empty, MHD_RESPMEM_PERSISTENT);
if (file) {
file->Destroy();
file = nullptr;
}
if (false && server->context.prefs->GetBool(
prefs::transcoder_synchronous_fallback.c_str(),
defaults::transcoder_synchronous_fallback))
{
/* if we're allowed, fall back to synchronous transcoding. we'll block
here until the entire file has been converted and cached */
file = Transcoder::TranscodeAndWait(server->context, filename, bitrate);
range = parseRange(file, rangeVal);
}
else {
/* otherwise fail with a "range not satisfiable" status */
status = 416;
char empty[1];
response = MHD_create_response_from_buffer(0, empty, MHD_RESPMEM_PERSISTENT);
}
}
}
}
@ -394,6 +406,9 @@ int HttpServer::HandleRequest(
if (!isOnDemandTranscoder) {
MHD_add_response_header(response, "Accept-Ranges", "bytes");
}
else {
MHD_add_response_header(response, "X-musikcube-Estimated-Content-Length", "true");
}
if (byExternalId) {
/* if we're using an on-demand transcoder, ensure the client does not cache the