mirror of
https://github.com/clangen/musikcube.git
synced 2025-02-07 06:40:36 +00:00
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:
parent
a5b7ab5107
commit
ab11376356
@ -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'
|
||||
|
BIN
src/musikdroid/app/libs/videocache-2.8.0-pre.aar
Normal file
BIN
src/musikdroid/app/libs/videocache-2.8.0-pre.aar
Normal file
Binary file not shown.
@ -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() {
|
||||
|
@ -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:
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user