3 Commits

Author SHA1 Message Date
Alexey Danilov
079bb4db6f use exoPlayer in sample 2017-05-18 11:22:30 +03:00
Lucas Nelaupe
acdf747a99 #20: headers injector 2017-05-18 10:30:57 +03:00
Alexey Danilov
89ab5937c6 notice about exoPlayer demo in exoPlayer branch 2017-04-24 13:06:06 +03:00
16 changed files with 191 additions and 198 deletions

View File

@@ -9,6 +9,8 @@
- [Disk cache limit](#disk-cache-limit)
- [Listen caching progress](#listen-caching-progress)
- [Providing names for cached files](#providing-names-for-cached-files)
- [Adding custom http headers](#adding-custom-http-headers)
- [Using exoPlayer](#using-exoplayer)
- [Sample](#sample)
- [Known problems](#known-problems)
- [Whats new](#whats-new)
@@ -19,7 +21,7 @@
## Why AndroidVideoCache?
Because there is no sense to download video a lot of times while streaming!
`AndroidVideoCache` allows to add caching support to your `VideoView/MediaPlayer`, [ExoPlayer](https://github.com/danikula/ExoPlayer/commit/6110be8559f003f98020ada8c5e09691b67aaff4) or any another player with help of single line!
`AndroidVideoCache` allows to add caching support to your `VideoView/MediaPlayer`, [ExoPlayer](https://github.com/danikula/AndroidVideoCache/tree/exoPlayer) or any another player with help of single line!
## Features
- caching to disk during streaming;
@@ -136,6 +138,28 @@ HttpProxyCacheServer proxy = HttpProxyCacheServer.Builder(context)
.build()
```
### Adding custom http headers
You can add custom headers to requests with help of `HeadersInjector`:
``` java
public class UserAgentHeadersInjector implements HeaderInjector {
@Override
public Map<String, String> addHeaders(String url) {
return Maps.newHashMap("User-Agent", "Cool app v1.1");
}
}
private HttpProxyCacheServer newProxy() {
return new HttpProxyCacheServer.Builder(this)
.headerInjector(new UserAgentHeadersInjector())
.build();
}
```
### Using exoPlayer
You can use [`exoPlayer`](https://google.github.io/ExoPlayer/) with `AndroidVideoCache`. See `sample` app in [`exoPlayer`](https://github.com/danikula/AndroidVideoCache/tree/exoPlayer) branch. Note [exoPlayer supports](https://github.com/google/ExoPlayer/commit/bd7be1b5e7cc41a59ebbc348d394820fc857db92) cache as well.
### Sample
See `sample` app.

View File

@@ -2,6 +2,7 @@ package com.danikula.videocache;
import com.danikula.videocache.file.DiskUsage;
import com.danikula.videocache.file.FileNameGenerator;
import com.danikula.videocache.headers.HeaderInjector;
import com.danikula.videocache.sourcestorage.SourceInfoStorage;
import java.io.File;
@@ -17,12 +18,14 @@ class Config {
public final FileNameGenerator fileNameGenerator;
public final DiskUsage diskUsage;
public final SourceInfoStorage sourceInfoStorage;
public final HeaderInjector headerInjector;
Config(File cacheRoot, FileNameGenerator fileNameGenerator, DiskUsage diskUsage, SourceInfoStorage sourceInfoStorage) {
Config(File cacheRoot, FileNameGenerator fileNameGenerator, DiskUsage diskUsage, SourceInfoStorage sourceInfoStorage, HeaderInjector headerInjector) {
this.cacheRoot = cacheRoot;
this.fileNameGenerator = fileNameGenerator;
this.diskUsage = diskUsage;
this.sourceInfoStorage = sourceInfoStorage;
this.headerInjector = headerInjector;
}
File generateCacheFile(String url) {

View File

@@ -8,6 +8,8 @@ import com.danikula.videocache.file.FileNameGenerator;
import com.danikula.videocache.file.Md5FileNameGenerator;
import com.danikula.videocache.file.TotalCountLruDiskUsage;
import com.danikula.videocache.file.TotalSizeLruDiskUsage;
import com.danikula.videocache.headers.EmptyHeadersInjector;
import com.danikula.videocache.headers.HeaderInjector;
import com.danikula.videocache.sourcestorage.SourceInfoStorage;
import com.danikula.videocache.sourcestorage.SourceInfoStorageFactory;
@@ -350,12 +352,14 @@ public class HttpProxyCacheServer {
private FileNameGenerator fileNameGenerator;
private DiskUsage diskUsage;
private SourceInfoStorage sourceInfoStorage;
private HeaderInjector headerInjector;
public Builder(Context context) {
this.sourceInfoStorage = SourceInfoStorageFactory.newSourceInfoStorage(context);
this.cacheRoot = StorageUtils.getIndividualCacheDirectory(context);
this.diskUsage = new TotalSizeLruDiskUsage(DEFAULT_MAX_SIZE);
this.fileNameGenerator = new Md5FileNameGenerator();
this.headerInjector = new EmptyHeadersInjector();
}
/**
@@ -426,6 +430,17 @@ public class HttpProxyCacheServer {
return this;
}
/**
* Add headers along the request to the server
*
* @param headerInjector to inject header base on url
* @return a builder
*/
public Builder headerInjector(HeaderInjector headerInjector) {
this.headerInjector = checkNotNull(headerInjector);
return this;
}
/**
* Builds new instance of {@link HttpProxyCacheServer}.
*
@@ -437,7 +452,7 @@ public class HttpProxyCacheServer {
}
private Config buildConfig() {
return new Config(cacheRoot, fileNameGenerator, diskUsage, sourceInfoStorage);
return new Config(cacheRoot, fileNameGenerator, diskUsage, sourceInfoStorage, headerInjector);
}
}

View File

@@ -79,7 +79,7 @@ final class HttpProxyCacheServerClients {
}
private HttpProxyCache newHttpProxyCache() throws ProxyCacheException {
HttpUrlSource source = new HttpUrlSource(url, config.sourceInfoStorage);
HttpUrlSource source = new HttpUrlSource(url, config.sourceInfoStorage, config.headerInjector);
FileCache cache = new FileCache(config.generateCacheFile(url), config.diskUsage);
HttpProxyCache httpProxyCache = new HttpProxyCache(source, cache);
httpProxyCache.registerCacheListener(uiCacheListener);

View File

@@ -2,6 +2,8 @@ package com.danikula.videocache;
import android.text.TextUtils;
import com.danikula.videocache.headers.EmptyHeadersInjector;
import com.danikula.videocache.headers.HeaderInjector;
import com.danikula.videocache.sourcestorage.SourceInfoStorage;
import com.danikula.videocache.sourcestorage.SourceInfoStorageFactory;
@@ -14,6 +16,7 @@ import java.io.InputStream;
import java.io.InterruptedIOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Map;
import static com.danikula.videocache.Preconditions.checkNotNull;
import static com.danikula.videocache.ProxyCacheUtils.DEFAULT_BUFFER_SIZE;
@@ -34,6 +37,7 @@ public class HttpUrlSource implements Source {
private static final int MAX_REDIRECTS = 5;
private final SourceInfoStorage sourceInfoStorage;
private final HeaderInjector headerInjector;
private SourceInfo sourceInfo;
private HttpURLConnection connection;
private InputStream inputStream;
@@ -43,7 +47,12 @@ public class HttpUrlSource implements Source {
}
public HttpUrlSource(String url, SourceInfoStorage sourceInfoStorage) {
this(url, sourceInfoStorage, new EmptyHeadersInjector());
}
public HttpUrlSource(String url, SourceInfoStorage sourceInfoStorage, HeaderInjector headerInjector) {
this.sourceInfoStorage = checkNotNull(sourceInfoStorage);
this.headerInjector = checkNotNull(headerInjector);
SourceInfo sourceInfo = sourceInfoStorage.get(url);
this.sourceInfo = sourceInfo != null ? sourceInfo :
new SourceInfo(url, Integer.MIN_VALUE, ProxyCacheUtils.getSupposablyMime(url));
@@ -52,6 +61,7 @@ public class HttpUrlSource implements Source {
public HttpUrlSource(HttpUrlSource source) {
this.sourceInfo = source.sourceInfo;
this.sourceInfoStorage = source.sourceInfoStorage;
this.headerInjector = source.headerInjector;
}
@Override
@@ -150,6 +160,7 @@ public class HttpUrlSource implements Source {
do {
LOG.debug("Open connection " + (offset > 0 ? " with offset " + offset : "") + " to " + url);
connection = (HttpURLConnection) new URL(url).openConnection();
injectCustomHeaders(connection, url);
if (offset > 0) {
connection.setRequestProperty("Range", "bytes=" + offset + "-");
}
@@ -171,6 +182,13 @@ public class HttpUrlSource implements Source {
return connection;
}
private void injectCustomHeaders(HttpURLConnection connection, String url) {
Map<String, String> extraHeaders = headerInjector.addHeaders(url);
for (Map.Entry<String, String> header : extraHeaders.entrySet()) {
connection.setRequestProperty(header.getKey(), header.getValue());
}
}
public synchronized String getMime() throws ProxyCacheException {
if (TextUtils.isEmpty(sourceInfo.mime)) {
fetchContentInfo();

View File

@@ -0,0 +1,18 @@
package com.danikula.videocache.headers;
import java.util.HashMap;
import java.util.Map;
/**
* Empty {@link HeaderInjector} implementation.
*
* @author Lucas Nelaupe (https://github.com/lucas34).
*/
public class EmptyHeadersInjector implements HeaderInjector {
@Override
public Map<String, String> addHeaders(String url) {
return new HashMap<>();
}
}

View File

@@ -0,0 +1,20 @@
package com.danikula.videocache.headers;
import java.util.Map;
/**
* Allows to add custom headers to server's requests.
*
* @author Lucas Nelaupe (https://github.com/lucas34).
*/
public interface HeaderInjector {
/**
* Adds headers to server's requests for corresponding url.
*
* @param url an url headers will be added for
* @return a map with headers, where keys are header's names, and values are header's values. {@code null} is not acceptable!
*/
Map<String, String> addHeaders(String url);
}

View File

@@ -36,6 +36,7 @@ apt {
dependencies {
// compile project(':library')
compile 'com.google.android.exoplayer:exoplayer:r2.3.1'
compile 'com.android.support:support-v4:23.1.0'
compile 'org.androidannotations:androidannotations-api:3.3.2'
compile 'com.danikula:videocache:2.7.0'

View File

@@ -22,7 +22,6 @@
</activity>
<activity android:name=".SingleVideoActivity_" />
<activity android:name=".MultipleVideosActivity_" />
<activity android:name=".VideoGalleryActivity_" />
<activity android:name=".SharedCacheActivity_" />
</application>

View File

@@ -1,130 +0,0 @@
package com.danikula.videocache.sample;
import android.os.Handler;
import android.os.Message;
import android.support.v4.app.Fragment;
import android.widget.ProgressBar;
import android.widget.VideoView;
import com.danikula.videocache.CacheListener;
import com.danikula.videocache.HttpProxyCacheServer;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.FragmentArg;
import org.androidannotations.annotations.InstanceState;
import org.androidannotations.annotations.SeekBarTouchStop;
import org.androidannotations.annotations.ViewById;
import java.io.File;
@EFragment(R.layout.fragment_video)
public class GalleryVideoFragment extends Fragment implements CacheListener {
@FragmentArg String url;
@InstanceState int position;
@InstanceState boolean playerStarted;
@ViewById VideoView videoView;
@ViewById ProgressBar progressBar;
private boolean visibleForUser;
private final VideoProgressUpdater updater = new VideoProgressUpdater();
public static Fragment build(String url) {
return GalleryVideoFragment_.builder()
.url(url)
.build();
}
@AfterViews
void afterViewInjected() {
startProxy();
if (visibleForUser) {
startPlayer();
}
}
private void startPlayer() {
videoView.seekTo(position);
videoView.start();
playerStarted = true;
}
private void startProxy() {
HttpProxyCacheServer proxy = App.getProxy(getActivity());
proxy.registerCacheListener(this, url);
videoView.setVideoPath(proxy.getProxyUrl(url));
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
visibleForUser = isVisibleToUser;
if (videoView != null) {
if (visibleForUser) {
startPlayer();
} else if (playerStarted) {
position = videoView.getCurrentPosition();
videoView.pause();
}
}
}
@Override
public void onResume() {
super.onResume();
updater.start();
}
@Override
public void onPause() {
super.onPause();
updater.stop();
}
@Override
public void onDestroy() {
super.onDestroy();
videoView.stopPlayback();
App.getProxy(getActivity()).unregisterCacheListener(this);
}
@Override
public void onCacheAvailable(File file, String url, int percentsAvailable) {
progressBar.setSecondaryProgress(percentsAvailable);
}
private void updateVideoProgress() {
int videoProgress = videoView.getCurrentPosition() * 100 / videoView.getDuration();
progressBar.setProgress(videoProgress);
}
@SeekBarTouchStop(R.id.progressBar)
void seekVideo() {
int videoPosition = videoView.getDuration() * progressBar.getProgress() / 100;
videoView.seekTo(videoPosition);
}
private final class VideoProgressUpdater extends Handler {
public void start() {
sendEmptyMessage(0);
}
public void stop() {
removeMessages(0);
}
@Override
public void handleMessage(Message msg) {
updateVideoProgress();
sendEmptyMessageDelayed(0, 500);
}
}
}

View File

@@ -35,7 +35,6 @@ public class MenuActivity extends FragmentActivity {
return Arrays.asList(
new ListEntry("Single Video", SingleVideoActivity_.class),
new ListEntry("Multiple Videos", MultipleVideosActivity_.class),
new ListEntry("Video Gallery with pre-caching", VideoGalleryActivity_.class),
new ListEntry("Shared Cache", SharedCacheActivity_.class)
);
}

View File

@@ -1,15 +1,33 @@
package com.danikula.videocache.sample;
import android.net.Uri;
import android.os.Handler;
import android.os.Message;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.VideoView;
import com.danikula.videocache.CacheListener;
import com.danikula.videocache.HttpProxyCacheServer;
import com.google.android.exoplayer2.DefaultLoadControl;
import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.LoadControl;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.ui.SimpleExoPlayerView;
import com.google.android.exoplayer2.upstream.BandwidthMeter;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.util.Util;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.EFragment;
@@ -27,8 +45,9 @@ public class VideoFragment extends Fragment implements CacheListener {
@FragmentArg String url;
@ViewById ImageView cacheStatusImageView;
@ViewById VideoView videoView;
@ViewById SimpleExoPlayerView simpleExoPlayerView;
@ViewById ProgressBar progressBar;
private SimpleExoPlayer simpleExoPlayer;
private final VideoProgressUpdater updater = new VideoProgressUpdater();
@@ -41,7 +60,8 @@ public class VideoFragment extends Fragment implements CacheListener {
@AfterViews
void afterViewInjected() {
checkCachedState();
startVideo();
simpleExoPlayer = setupPlayer();
simpleExoPlayer.setPlayWhenReady(true);
}
private void checkCachedState() {
@@ -53,32 +73,57 @@ public class VideoFragment extends Fragment implements CacheListener {
}
}
private void startVideo() {
private SimpleExoPlayer setupPlayer() {
simpleExoPlayerView.setUseController(false);
HttpProxyCacheServer proxy = App.getProxy(getActivity());
proxy.registerCacheListener(this, url);
String proxyUrl = proxy.getProxyUrl(url);
Log.d(LOG_TAG, "Use proxy url " + proxyUrl + " instead of original url " + url);
videoView.setVideoPath(proxyUrl);
videoView.start();
SimpleExoPlayer exoPlayer = newSimpleExoPlayer();
simpleExoPlayerView.setPlayer(exoPlayer);
MediaSource videoSource = newVideoSource(proxyUrl);
exoPlayer.prepare(videoSource);
return exoPlayer;
}
private SimpleExoPlayer newSimpleExoPlayer() {
BandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
TrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory(bandwidthMeter);
TrackSelector trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory);
LoadControl loadControl = new DefaultLoadControl();
return ExoPlayerFactory.newSimpleInstance(getActivity(), trackSelector, loadControl);
}
private MediaSource newVideoSource(String url) {
DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
String userAgent = Util.getUserAgent(getActivity(), "AndroidVideoCache sample");
DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(getActivity(), userAgent, bandwidthMeter);
ExtractorsFactory extractorsFactory = new DefaultExtractorsFactory();
return new ExtractorMediaSource(Uri.parse(url), dataSourceFactory, extractorsFactory, null, null);
}
@Override
public void onResume() {
super.onResume();
updater.start();
simpleExoPlayer.setPlayWhenReady(true);
}
@Override
public void onPause() {
super.onPause();
updater.stop();
simpleExoPlayer.setPlayWhenReady(false);
}
@Override
public void onDestroy() {
super.onDestroy();
videoView.stopPlayback();
simpleExoPlayer.release();
App.getProxy(getActivity()).unregisterCacheListener(this);
}
@@ -90,14 +135,14 @@ public class VideoFragment extends Fragment implements CacheListener {
}
private void updateVideoProgress() {
int videoProgress = videoView.getCurrentPosition() * 100 / videoView.getDuration();
progressBar.setProgress(videoProgress);
long videoProgress = simpleExoPlayer.getCurrentPosition() * 100 / simpleExoPlayer.getDuration();
progressBar.setProgress((int) videoProgress);
}
@SeekBarTouchStop(R.id.progressBar)
void seekVideo() {
int videoPosition = videoView.getDuration() * progressBar.getProgress() / 100;
videoView.seekTo(videoPosition);
long videoPosition = simpleExoPlayer.getDuration() * progressBar.getProgress() / 100;
simpleExoPlayer.seekTo(videoPosition);
}
private void setCachedState(boolean cached) {

View File

@@ -1,49 +0,0 @@
package com.danikula.videocache.sample;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentStatePagerAdapter;
import android.support.v4.view.ViewPager;
import com.viewpagerindicator.CirclePageIndicator;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.ViewById;
@EActivity(R.layout.activity_video_gallery)
public class VideoGalleryActivity extends FragmentActivity {
@ViewById ViewPager viewPager;
@ViewById CirclePageIndicator viewPagerIndicator;
@AfterViews
void afterViewInjected() {
ViewsPagerAdapter viewsPagerAdapter = new ViewsPagerAdapter(this);
viewPager.setAdapter(viewsPagerAdapter);
viewPagerIndicator.setViewPager(viewPager);
}
private static final class ViewsPagerAdapter extends FragmentStatePagerAdapter {
public ViewsPagerAdapter(FragmentActivity activity) {
super(activity.getSupportFragmentManager());
}
@Override
public Fragment getItem(int position) {
Video video = Video.values()[position];
return GalleryVideoFragment.build(video.url);
}
@Override
public int getCount() {
return Video.values().length;
}
@Override
public CharSequence getPageTitle(int position) {
return Video.values()[position].name();
}
}
}

View File

@@ -5,8 +5,8 @@
android:layout_height="match_parent"
tools:context=".VideoActivity">
<VideoView
android:id="@+id/videoView"
<com.google.android.exoplayer2.ui.SimpleExoPlayerView
android:id="@+id/simpleExoPlayerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />

View File

@@ -6,11 +6,13 @@ import android.util.Pair;
import com.danikula.android.garden.io.IoUtils;
import com.danikula.videocache.file.FileNameGenerator;
import com.danikula.videocache.file.Md5FileNameGenerator;
import com.danikula.videocache.headers.HeaderInjector;
import com.danikula.videocache.support.ProxyCacheTestUtils;
import com.danikula.videocache.support.Response;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.robolectric.RuntimeEnvironment;
import java.io.File;
@@ -36,6 +38,8 @@ import static com.danikula.videocache.support.ProxyCacheTestUtils.loadAssetFile;
import static com.danikula.videocache.support.ProxyCacheTestUtils.readProxyResponse;
import static com.danikula.videocache.support.ProxyCacheTestUtils.resetSystemProxy;
import static org.fest.assertions.api.Assertions.assertThat;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
/**
* @author Alexey Danilov (danikula@gmail.com).
@@ -360,6 +364,20 @@ public class HttpProxyCacheServerTest extends BaseTest {
assertThat(proxiedUrl).isEqualTo(HTTP_DATA_URL);
}
@Test
public void testHeadersInjectorIsInvoked() throws Exception {
HeaderInjector mockedHeaderInjector = Mockito.mock(HeaderInjector.class);
HttpProxyCacheServer proxy = new HttpProxyCacheServer.Builder(RuntimeEnvironment.application)
.headerInjector(mockedHeaderInjector)
.build();
readProxyResponse(proxy, HTTP_DATA_URL);
proxy.shutdown();
verify(mockedHeaderInjector, times(2)).addHeaders(HTTP_DATA_URL); // content info & fetch data requests
}
private Pair<File, Response> readProxyData(String url, int offset) throws IOException {
File file = file(cacheFolder, url);
HttpProxyCacheServer proxy = newProxy(cacheFolder);

View File

@@ -1,5 +1,6 @@
package com.danikula.videocache;
import com.danikula.videocache.headers.HeaderInjector;
import com.danikula.videocache.sourcestorage.SourceInfoStorage;
import com.danikula.videocache.sourcestorage.SourceInfoStorageFactory;
import com.danikula.videocache.support.ProxyCacheTestUtils;
@@ -25,6 +26,7 @@ import static com.danikula.videocache.support.ProxyCacheTestUtils.loadAssetFile;
import static org.fest.assertions.api.Assertions.assertThat;
import static org.fest.assertions.api.Assertions.fail;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.when;
/**
* @author Alexey Danilov (danikula@gmail.com).
@@ -156,6 +158,16 @@ public class HttpUrlSourceTest extends BaseTest {
fail("source.open() should throw exception");
}
@Test(expected = NullPointerException.class)
public void testHeaderInjectorNullNotAcceptable() throws Exception {
HeaderInjector mockedHeaderInjector = Mockito.mock(HeaderInjector.class);
when(mockedHeaderInjector.addHeaders(Mockito.anyString())).thenReturn(null);
SourceInfoStorage emptySourceInfoStorage = SourceInfoStorageFactory.newEmptySourceInfoStorage();
HttpUrlSource source = new HttpUrlSource(HTTP_DATA_URL_ONE_REDIRECT, emptySourceInfoStorage, mockedHeaderInjector);
source.open(0);
fail("source.open should throw NPE!");
}
private void readSource(Source source, byte[] target) throws ProxyCacheException {
byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
int totalRead = 0;