diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java
deleted file mode 100644
index ec45ea01c7..0000000000
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * 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 com.google.android.exoplayer2.offline;
-
-import android.test.InstrumentationTestCase;
-import com.google.android.exoplayer2.testutil.TestUtil;
-import com.google.android.exoplayer2.upstream.DummyDataSource;
-import com.google.android.exoplayer2.upstream.cache.Cache;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.DataInputStream;
-import java.io.DataOutputStream;
-import java.io.IOException;
-import org.mockito.Mockito;
-
-/**
- * Unit tests for {@link ProgressiveDownloadAction}.
- */
-public class ProgressiveDownloadActionTest extends InstrumentationTestCase {
-
- public void testDownloadActionIsNotRemoveAction() throws Exception {
- ProgressiveDownloadAction action = new ProgressiveDownloadAction("uri", null, false);
- assertFalse(action.isRemoveAction());
- }
-
- public void testRemoveActionIsRemoveAction() throws Exception {
- ProgressiveDownloadAction action2 = new ProgressiveDownloadAction("uri", null, true);
- assertTrue(action2.isRemoveAction());
- }
-
- public void testCreateDownloader() throws Exception {
- TestUtil.setUpMockito(this);
- ProgressiveDownloadAction action = new ProgressiveDownloadAction("uri", null, false);
- DownloaderConstructorHelper constructorHelper = new DownloaderConstructorHelper(
- Mockito.mock(Cache.class), DummyDataSource.FACTORY);
- assertNotNull(action.createDownloader(constructorHelper));
- }
-
- public void testSameUriCacheKeyDifferentAction_IsSameMedia() throws Exception {
- ProgressiveDownloadAction action1 = new ProgressiveDownloadAction("uri", null, true);
- ProgressiveDownloadAction action2 = new ProgressiveDownloadAction("uri", null, false);
- assertTrue(action1.isSameMedia(action2));
- }
-
- public void testNullCacheKeyDifferentUriAction_IsNotSameMedia() throws Exception {
- ProgressiveDownloadAction action3 = new ProgressiveDownloadAction("uri2", null, true);
- ProgressiveDownloadAction action4 = new ProgressiveDownloadAction("uri", null, false);
- assertFalse(action3.isSameMedia(action4));
- }
-
- public void testSameCacheKeyDifferentUriAction_IsSameMedia() throws Exception {
- ProgressiveDownloadAction action5 = new ProgressiveDownloadAction("uri2", "key", true);
- ProgressiveDownloadAction action6 = new ProgressiveDownloadAction("uri", "key", false);
- assertTrue(action5.isSameMedia(action6));
- }
-
- public void testSameUriDifferentCacheKeyAction_IsNotSameMedia() throws Exception {
- ProgressiveDownloadAction action7 = new ProgressiveDownloadAction("uri", "key", true);
- ProgressiveDownloadAction action8 = new ProgressiveDownloadAction("uri", "key2", false);
- assertFalse(action7.isSameMedia(action8));
- }
-
- public void testEquals() throws Exception {
- ProgressiveDownloadAction action1 = new ProgressiveDownloadAction("uri", null, true);
- assertTrue(action1.equals(action1));
-
- ProgressiveDownloadAction action2 = new ProgressiveDownloadAction("uri", null, true);
- ProgressiveDownloadAction action3 = new ProgressiveDownloadAction("uri", null, true);
- assertTrue(action2.equals(action3));
-
- ProgressiveDownloadAction action4 = new ProgressiveDownloadAction("uri", null, true);
- ProgressiveDownloadAction action5 = new ProgressiveDownloadAction("uri", null, false);
- assertFalse(action4.equals(action5));
-
- ProgressiveDownloadAction action6 = new ProgressiveDownloadAction("uri", null, true);
- ProgressiveDownloadAction action7 = new ProgressiveDownloadAction("uri", "key", true);
- assertFalse(action6.equals(action7));
-
- ProgressiveDownloadAction action8 = new ProgressiveDownloadAction("uri", "key2", true);
- ProgressiveDownloadAction action9 = new ProgressiveDownloadAction("uri", "key", true);
- assertFalse(action8.equals(action9));
-
- ProgressiveDownloadAction action10 = new ProgressiveDownloadAction("uri", null, true);
- ProgressiveDownloadAction action11 = new ProgressiveDownloadAction("uri2", null, true);
- assertFalse(action10.equals(action11));
- }
-
- public void testSerializerGetType() throws Exception {
- ProgressiveDownloadAction action = new ProgressiveDownloadAction("uri", null, false);
- assertNotNull(action.getType());
- }
-
- public void testSerializerWriteRead() throws Exception {
- doTestSerializationRoundTrip(new ProgressiveDownloadAction("uri1", null, false));
- doTestSerializationRoundTrip(new ProgressiveDownloadAction("uri2", "key", true));
- }
-
- private void doTestSerializationRoundTrip(ProgressiveDownloadAction action1) throws IOException {
- ByteArrayOutputStream out = new ByteArrayOutputStream();
- DataOutputStream output = new DataOutputStream(out);
- action1.writeToStream(output);
-
- ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
- DataInputStream input = new DataInputStream(in);
- DownloadAction action2 = ProgressiveDownloadAction.DESERIALIZER.readFromStream(input);
-
- assertEquals(action1, action2);
- }
-
-}
diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadTestData.java b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadTestData.java
new file mode 100644
index 0000000000..220adfb3c5
--- /dev/null
+++ b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadTestData.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * 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 com.google.android.exoplayer2.source.dash.offline;
+
+import android.net.Uri;
+import com.google.android.exoplayer2.util.ClosedSource;
+
+/**
+ * Data for DASH downloading tests.
+ */
+@ClosedSource(reason = "Not ready yet")
+/* package */ interface DashDownloadTestData {
+
+ Uri TEST_MPD_URI = Uri.parse("test.mpd");
+
+ byte[] TEST_MPD =
+ ("\n"
+ + "\n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ // Bounded range data
+ + " \n"
+ // Unbounded range data
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ // This segment list has a 1 second offset to make sure the progressive download order
+ + " \n"
+ + " \n"
+ + " \n" // 1s offset
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + "").getBytes();
+
+ byte[] TEST_MPD_NO_INDEX =
+ ("\n"
+ + "\n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + "").getBytes();
+}
diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java
new file mode 100644
index 0000000000..f5e00d2cec
--- /dev/null
+++ b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java
@@ -0,0 +1,406 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * 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 com.google.android.exoplayer2.source.dash.offline;
+
+import static com.google.android.exoplayer2.source.dash.offline.DashDownloadTestData.TEST_MPD;
+import static com.google.android.exoplayer2.source.dash.offline.DashDownloadTestData.TEST_MPD_NO_INDEX;
+import static com.google.android.exoplayer2.source.dash.offline.DashDownloadTestData.TEST_MPD_URI;
+import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCacheEmpty;
+import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCachedData;
+import static com.google.android.exoplayer2.testutil.CacheAsserts.assertDataCached;
+
+import android.test.InstrumentationTestCase;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.offline.DownloadException;
+import com.google.android.exoplayer2.offline.Downloader.ProgressListener;
+import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
+import com.google.android.exoplayer2.source.dash.manifest.DashManifest;
+import com.google.android.exoplayer2.source.dash.manifest.RepresentationKey;
+import com.google.android.exoplayer2.testutil.FakeDataSet;
+import com.google.android.exoplayer2.testutil.FakeDataSource;
+import com.google.android.exoplayer2.testutil.FakeDataSource.Factory;
+import com.google.android.exoplayer2.testutil.TestUtil;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor;
+import com.google.android.exoplayer2.upstream.cache.SimpleCache;
+import com.google.android.exoplayer2.util.Util;
+import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
+import org.mockito.InOrder;
+import org.mockito.Mockito;
+
+/**
+ * Unit tests for {@link DashDownloader}.
+ */
+public class DashDownloaderTest extends InstrumentationTestCase {
+
+ private SimpleCache cache;
+ private File tempFolder;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ TestUtil.setUpMockito(this);
+ tempFolder = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
+ cache = new SimpleCache(tempFolder, new NoOpCacheEvictor());
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ Util.recursiveDelete(tempFolder);
+ super.tearDown();
+ }
+
+ public void testGetManifest() throws Exception {
+ FakeDataSet fakeDataSet = new FakeDataSet()
+ .setData(TEST_MPD_URI, TEST_MPD);
+ DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
+
+ DashManifest manifest = dashDownloader.getManifest();
+
+ assertNotNull(manifest);
+ assertCachedData(cache, fakeDataSet);
+ }
+
+ public void testDownloadManifestFailure() throws Exception {
+ byte[] testMpdFirstPart = Arrays.copyOf(TEST_MPD, 10);
+ byte[] testMpdSecondPart = Arrays.copyOfRange(TEST_MPD, 10, TEST_MPD.length);
+ FakeDataSet fakeDataSet = new FakeDataSet()
+ .newData(TEST_MPD_URI)
+ .appendReadData(testMpdFirstPart)
+ .appendReadError(new IOException())
+ .appendReadData(testMpdSecondPart)
+ .endData();
+ DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
+
+ // fails on the first try
+ try {
+ dashDownloader.getManifest();
+ fail();
+ } catch (IOException e) {
+ // ignore
+ }
+ assertDataCached(cache, TEST_MPD_URI, testMpdFirstPart);
+
+ // on the second try it downloads the rest of the data
+ DashManifest manifest = dashDownloader.getManifest();
+
+ assertNotNull(manifest);
+ assertCachedData(cache, fakeDataSet);
+ }
+
+ public void testDownloadRepresentation() throws Exception {
+ FakeDataSet fakeDataSet = new FakeDataSet()
+ .setData(TEST_MPD_URI, TEST_MPD)
+ .setRandomData("audio_init_data", 10)
+ .setRandomData("audio_segment_1", 4)
+ .setRandomData("audio_segment_2", 5)
+ .setRandomData("audio_segment_3", 6);
+ DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
+
+ dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)});
+ dashDownloader.download(null);
+
+ assertCachedData(cache, fakeDataSet);
+ }
+
+ public void testDownloadRepresentationInSmallParts() throws Exception {
+ FakeDataSet fakeDataSet = new FakeDataSet()
+ .setData(TEST_MPD_URI, TEST_MPD)
+ .setRandomData("audio_init_data", 10)
+ .newData("audio_segment_1")
+ .appendReadData(TestUtil.buildTestData(10))
+ .appendReadData(TestUtil.buildTestData(10))
+ .appendReadData(TestUtil.buildTestData(10))
+ .endData()
+ .setRandomData("audio_segment_2", 5)
+ .setRandomData("audio_segment_3", 6);
+ DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
+
+ dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)});
+ dashDownloader.download(null);
+
+ assertCachedData(cache, fakeDataSet);
+ }
+
+ public void testDownloadRepresentations() throws Exception {
+ FakeDataSet fakeDataSet = new FakeDataSet()
+ .setData(TEST_MPD_URI, TEST_MPD)
+ .setRandomData("audio_init_data", 10)
+ .setRandomData("audio_segment_1", 4)
+ .setRandomData("audio_segment_2", 5)
+ .setRandomData("audio_segment_3", 6)
+ .setRandomData("text_segment_1", 1)
+ .setRandomData("text_segment_2", 2)
+ .setRandomData("text_segment_3", 3);
+ DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
+
+ dashDownloader.selectRepresentations(
+ new RepresentationKey[] {new RepresentationKey(0, 0, 0), new RepresentationKey(0, 1, 0)});
+ dashDownloader.download(null);
+
+ assertCachedData(cache, fakeDataSet);
+ }
+
+ public void testDownloadAllRepresentations() throws Exception {
+ FakeDataSet fakeDataSet = new FakeDataSet()
+ .setData(TEST_MPD_URI, TEST_MPD)
+ .setRandomData("audio_init_data", 10)
+ .setRandomData("audio_segment_1", 4)
+ .setRandomData("audio_segment_2", 5)
+ .setRandomData("audio_segment_3", 6)
+ .setRandomData("text_segment_1", 1)
+ .setRandomData("text_segment_2", 2)
+ .setRandomData("text_segment_3", 3)
+ .setRandomData("period_2_segment_1", 1)
+ .setRandomData("period_2_segment_2", 2)
+ .setRandomData("period_2_segment_3", 3);
+ DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
+
+ // dashDownloader.selectRepresentations() isn't called
+ dashDownloader.download(null);
+ assertCachedData(cache, fakeDataSet);
+ dashDownloader.remove();
+
+ // select something random
+ dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)});
+ // clear selection
+ dashDownloader.selectRepresentations(null);
+ dashDownloader.download(null);
+ assertCachedData(cache, fakeDataSet);
+ dashDownloader.remove();
+
+ dashDownloader.selectRepresentations(new RepresentationKey[0]);
+ dashDownloader.download(null);
+ assertCachedData(cache, fakeDataSet);
+ dashDownloader.remove();
+ }
+
+ public void testProgressiveDownload() throws Exception {
+ FakeDataSet fakeDataSet = new FakeDataSet()
+ .setData(TEST_MPD_URI, TEST_MPD)
+ .setRandomData("audio_init_data", 10)
+ .setRandomData("audio_segment_1", 4)
+ .setRandomData("audio_segment_2", 5)
+ .setRandomData("audio_segment_3", 6)
+ .setRandomData("text_segment_1", 1)
+ .setRandomData("text_segment_2", 2)
+ .setRandomData("text_segment_3", 3);
+ FakeDataSource fakeDataSource = new FakeDataSource(fakeDataSet);
+ Factory factory = Mockito.mock(Factory.class);
+ Mockito.when(factory.createDataSource()).thenReturn(fakeDataSource);
+ DashDownloader dashDownloader = new DashDownloader(TEST_MPD_URI,
+ new DownloaderConstructorHelper(cache, factory));
+
+ dashDownloader.selectRepresentations(
+ new RepresentationKey[] {new RepresentationKey(0, 0, 0), new RepresentationKey(0, 1, 0)});
+ dashDownloader.download(null);
+
+ DataSpec[] openedDataSpecs = fakeDataSource.getAndClearOpenedDataSpecs();
+ assertEquals(8, openedDataSpecs.length);
+ assertEquals(TEST_MPD_URI, openedDataSpecs[0].uri);
+ assertEquals("audio_init_data", openedDataSpecs[1].uri.getPath());
+ assertEquals("audio_segment_1", openedDataSpecs[2].uri.getPath());
+ assertEquals("text_segment_1", openedDataSpecs[3].uri.getPath());
+ assertEquals("audio_segment_2", openedDataSpecs[4].uri.getPath());
+ assertEquals("text_segment_2", openedDataSpecs[5].uri.getPath());
+ assertEquals("audio_segment_3", openedDataSpecs[6].uri.getPath());
+ assertEquals("text_segment_3", openedDataSpecs[7].uri.getPath());
+ }
+
+ public void testProgressiveDownloadSeparatePeriods() throws Exception {
+ FakeDataSet fakeDataSet = new FakeDataSet()
+ .setData(TEST_MPD_URI, TEST_MPD)
+ .setRandomData("audio_init_data", 10)
+ .setRandomData("audio_segment_1", 4)
+ .setRandomData("audio_segment_2", 5)
+ .setRandomData("audio_segment_3", 6)
+ .setRandomData("period_2_segment_1", 1)
+ .setRandomData("period_2_segment_2", 2)
+ .setRandomData("period_2_segment_3", 3);
+ FakeDataSource fakeDataSource = new FakeDataSource(fakeDataSet);
+ Factory factory = Mockito.mock(Factory.class);
+ Mockito.when(factory.createDataSource()).thenReturn(fakeDataSource);
+ DashDownloader dashDownloader = new DashDownloader(TEST_MPD_URI,
+ new DownloaderConstructorHelper(cache, factory));
+
+ dashDownloader.selectRepresentations(
+ new RepresentationKey[] {new RepresentationKey(0, 0, 0), new RepresentationKey(1, 0, 0)});
+ dashDownloader.download(null);
+
+ DataSpec[] openedDataSpecs = fakeDataSource.getAndClearOpenedDataSpecs();
+ assertEquals(8, openedDataSpecs.length);
+ assertEquals(TEST_MPD_URI, openedDataSpecs[0].uri);
+ assertEquals("audio_init_data", openedDataSpecs[1].uri.getPath());
+ assertEquals("audio_segment_1", openedDataSpecs[2].uri.getPath());
+ assertEquals("audio_segment_2", openedDataSpecs[3].uri.getPath());
+ assertEquals("audio_segment_3", openedDataSpecs[4].uri.getPath());
+ assertEquals("period_2_segment_1", openedDataSpecs[5].uri.getPath());
+ assertEquals("period_2_segment_2", openedDataSpecs[6].uri.getPath());
+ assertEquals("period_2_segment_3", openedDataSpecs[7].uri.getPath());
+ }
+
+ public void testDownloadRepresentationFailure() throws Exception {
+ FakeDataSet fakeDataSet = new FakeDataSet()
+ .setData(TEST_MPD_URI, TEST_MPD)
+ .setRandomData("audio_init_data", 10)
+ .setRandomData("audio_segment_1", 4)
+ .newData("audio_segment_2")
+ .appendReadData(TestUtil.buildTestData(2))
+ .appendReadError(new IOException())
+ .appendReadData(TestUtil.buildTestData(3))
+ .endData()
+ .setRandomData("audio_segment_3", 6);
+ DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
+
+ dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)});
+ // downloadRepresentations fails on the first try
+ try {
+ dashDownloader.download(null);
+ fail();
+ } catch (IOException e) {
+ // ignore
+ }
+ dashDownloader.download(null);
+
+ assertCachedData(cache, fakeDataSet);
+ }
+
+ public void testCounters() throws Exception {
+ FakeDataSet fakeDataSet = new FakeDataSet()
+ .setData(TEST_MPD_URI, TEST_MPD)
+ .setRandomData("audio_init_data", 10)
+ .setRandomData("audio_segment_1", 4)
+ .newData("audio_segment_2")
+ .appendReadData(TestUtil.buildTestData(2))
+ .appendReadError(new IOException())
+ .appendReadData(TestUtil.buildTestData(3))
+ .endData()
+ .setRandomData("audio_segment_3", 6);
+ DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
+
+ assertCounters(dashDownloader, C.LENGTH_UNSET, C.LENGTH_UNSET, C.LENGTH_UNSET);
+
+ dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)});
+ dashDownloader.init();
+ assertCounters(dashDownloader, C.LENGTH_UNSET, C.LENGTH_UNSET, C.LENGTH_UNSET);
+
+ // downloadRepresentations fails after downloading init data, segment 1 and 2 bytes in segment 2
+ try {
+ dashDownloader.download(null);
+ fail();
+ } catch (IOException e) {
+ // ignore
+ }
+ dashDownloader.init();
+ assertCounters(dashDownloader, 4, 2, 10 + 4 + 2);
+
+ dashDownloader.download(null);
+
+ assertCounters(dashDownloader, 4, 4, 10 + 4 + 5 + 6);
+ }
+
+ public void testListener() throws Exception {
+ FakeDataSet fakeDataSet = new FakeDataSet()
+ .setData(TEST_MPD_URI, TEST_MPD)
+ .setRandomData("audio_init_data", 10)
+ .setRandomData("audio_segment_1", 4)
+ .setRandomData("audio_segment_2", 5)
+ .setRandomData("audio_segment_3", 6);
+ DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
+
+ dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)});
+ ProgressListener mockListener = Mockito.mock(ProgressListener.class);
+ dashDownloader.download(mockListener);
+ InOrder inOrder = Mockito.inOrder(mockListener);
+ inOrder.verify(mockListener).onDownloadProgress(dashDownloader, 0.0f, 0);
+ inOrder.verify(mockListener).onDownloadProgress(dashDownloader, 25.0f, 10);
+ inOrder.verify(mockListener).onDownloadProgress(dashDownloader, 50.0f, 14);
+ inOrder.verify(mockListener).onDownloadProgress(dashDownloader, 75.0f, 19);
+ inOrder.verify(mockListener).onDownloadProgress(dashDownloader, 100.0f, 25);
+ inOrder.verifyNoMoreInteractions();
+ }
+
+ public void testRemoveAll() throws Exception {
+ FakeDataSet fakeDataSet = new FakeDataSet()
+ .setData(TEST_MPD_URI, TEST_MPD)
+ .setRandomData("audio_init_data", 10)
+ .setRandomData("audio_segment_1", 4)
+ .setRandomData("audio_segment_2", 5)
+ .setRandomData("audio_segment_3", 6)
+ .setRandomData("text_segment_1", 1)
+ .setRandomData("text_segment_2", 2)
+ .setRandomData("text_segment_3", 3);
+ DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
+ dashDownloader.selectRepresentations(
+ new RepresentationKey[] {new RepresentationKey(0, 0, 0), new RepresentationKey(0, 1, 0)});
+ dashDownloader.download(null);
+
+ dashDownloader.remove();
+
+ assertCacheEmpty(cache);
+ }
+
+ public void testRepresentationWithoutIndex() throws Exception {
+ FakeDataSet fakeDataSet = new FakeDataSet()
+ .setData(TEST_MPD_URI, TEST_MPD_NO_INDEX)
+ .setRandomData("test_segment_1", 4);
+ DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
+
+ dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)});
+ dashDownloader.init();
+ try {
+ dashDownloader.download(null);
+ fail();
+ } catch (DownloadException e) {
+ // expected exception.
+ }
+ dashDownloader.remove();
+
+ assertCacheEmpty(cache);
+ }
+
+ public void testSelectRepresentationsClearsPreviousSelection() throws Exception {
+ FakeDataSet fakeDataSet = new FakeDataSet()
+ .setData(TEST_MPD_URI, TEST_MPD)
+ .setRandomData("audio_init_data", 10)
+ .setRandomData("audio_segment_1", 4)
+ .setRandomData("audio_segment_2", 5)
+ .setRandomData("audio_segment_3", 6);
+ DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
+
+ dashDownloader.selectRepresentations(
+ new RepresentationKey[] {new RepresentationKey(0, 0, 0), new RepresentationKey(0, 1, 0)});
+ dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)});
+ dashDownloader.download(null);
+
+ assertCachedData(cache, fakeDataSet);
+ }
+
+ private DashDownloader getDashDownloader(FakeDataSet fakeDataSet) {
+ Factory factory = new Factory(null).setFakeDataSet(fakeDataSet);
+ return new DashDownloader(TEST_MPD_URI, new DownloaderConstructorHelper(cache, factory));
+ }
+
+ private static void assertCounters(DashDownloader dashDownloader, int totalSegments,
+ int downloadedSegments, int downloadedBytes) {
+ assertEquals(totalSegments, dashDownloader.getTotalSegments());
+ assertEquals(downloadedSegments, dashDownloader.getDownloadedSegments());
+ assertEquals(downloadedBytes, dashDownloader.getDownloadedBytes());
+ }
+
+}
diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java
new file mode 100644
index 0000000000..558adca7bd
--- /dev/null
+++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * 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 com.google.android.exoplayer2.source.dash.offline;
+
+import android.net.Uri;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.extractor.ChunkIndex;
+import com.google.android.exoplayer2.offline.DownloadException;
+import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
+import com.google.android.exoplayer2.offline.SegmentDownloader;
+import com.google.android.exoplayer2.source.dash.DashSegmentIndex;
+import com.google.android.exoplayer2.source.dash.DashUtil;
+import com.google.android.exoplayer2.source.dash.DashWrappingSegmentIndex;
+import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet;
+import com.google.android.exoplayer2.source.dash.manifest.DashManifest;
+import com.google.android.exoplayer2.source.dash.manifest.Period;
+import com.google.android.exoplayer2.source.dash.manifest.RangedUri;
+import com.google.android.exoplayer2.source.dash.manifest.Representation;
+import com.google.android.exoplayer2.source.dash.manifest.RepresentationKey;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Helper class to download DASH streams.
+ *
+ *
Except {@link #getTotalSegments()}, {@link #getDownloadedSegments()} and {@link
+ * #getDownloadedBytes()}, this class isn't thread safe.
+ *
+ *
Example usage:
+ *
+ *
+ * {@code
+ * SimpleCache cache = new SimpleCache(downloadFolder, new NoOpCacheEvictor());
+ * DefaultHttpDataSourceFactory factory = new DefaultHttpDataSourceFactory("ExoPlayer", null);
+ * DownloaderConstructorHelper constructorHelper =
+ * new DownloaderConstructorHelper(cache, factory);
+ * DashDownloader dashDownloader = new DashDownloader(manifestUrl, constructorHelper);
+ * // Select the first representation of the first adaptation set of the first period
+ * dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)});
+ * dashDownloader.download(new ProgressListener() {
+ * @Override
+ * public void onDownloadProgress(Downloader downloader, float downloadPercentage,
+ * long downloadedBytes) {
+ * // Invoked periodically during the download.
+ * }
+ * });
+ * // Access downloaded data using CacheDataSource
+ * CacheDataSource cacheDataSource =
+ * new CacheDataSource(cache, factory.createDataSource(), CacheDataSource.FLAG_BLOCK_ON_CACHE);}
+ *
+ */
+public final class DashDownloader extends SegmentDownloader {
+
+ /**
+ * @see SegmentDownloader#SegmentDownloader(Uri, DownloaderConstructorHelper)
+ */
+ public DashDownloader(Uri manifestUri, DownloaderConstructorHelper constructorHelper) {
+ super(manifestUri, constructorHelper);
+ }
+
+ @Override
+ public DashManifest getManifest(DataSource dataSource, Uri uri) throws IOException {
+ return DashUtil.loadManifest(dataSource, uri);
+ }
+
+ @Override
+ protected List getAllSegments(DataSource dataSource, DashManifest manifest,
+ boolean allowIndexLoadErrors) throws InterruptedException, IOException {
+ ArrayList segments = new ArrayList<>();
+ for (int periodIndex = 0; periodIndex < manifest.getPeriodCount(); periodIndex++) {
+ List adaptationSets = manifest.getPeriod(periodIndex).adaptationSets;
+ for (int adaptationIndex = 0; adaptationIndex < adaptationSets.size(); adaptationIndex++) {
+ AdaptationSet adaptationSet = adaptationSets.get(adaptationIndex);
+ RepresentationKey[] keys = new RepresentationKey[adaptationSet.representations.size()];
+ for (int i = 0; i < keys.length; i++) {
+ keys[i] = new RepresentationKey(periodIndex, adaptationIndex, i);
+ }
+ segments.addAll(getSegments(dataSource, manifest, keys, allowIndexLoadErrors));
+ }
+ }
+ return segments;
+ }
+
+ @Override
+ protected List getSegments(DataSource dataSource, DashManifest manifest,
+ RepresentationKey[] keys, boolean allowIndexLoadErrors)
+ throws InterruptedException, IOException {
+ ArrayList segments = new ArrayList<>();
+ for (RepresentationKey key : keys) {
+ DashSegmentIndex index;
+ try {
+ index = getSegmentIndex(dataSource, manifest, key);
+ if (index == null) {
+ // Loading succeeded but there was no index. This is always a failure.
+ throw new DownloadException("No index for representation: " + key);
+ }
+ } catch (IOException e) {
+ if (allowIndexLoadErrors) {
+ // Loading failed, but load errors are allowed. Advance to the next key.
+ continue;
+ } else {
+ throw e;
+ }
+ }
+
+ int segmentCount = index.getSegmentCount(C.TIME_UNSET);
+ if (segmentCount == DashSegmentIndex.INDEX_UNBOUNDED) {
+ throw new DownloadException("Unbounded index for representation: " + key);
+ }
+
+ Period period = manifest.getPeriod(key.periodIndex);
+ Representation representation = period.adaptationSets.get(key.adaptationSetIndex)
+ .representations.get(key.representationIndex);
+ long startUs = C.msToUs(period.startMs);
+ String baseUrl = representation.baseUrl;
+ RangedUri initializationUri = representation.getInitializationUri();
+ if (initializationUri != null) {
+ addSegment(segments, startUs, baseUrl, initializationUri);
+ }
+ RangedUri indexUri = representation.getIndexUri();
+ if (indexUri != null) {
+ addSegment(segments, startUs, baseUrl, indexUri);
+ }
+
+ int firstSegmentNum = index.getFirstSegmentNum();
+ int lastSegmentNum = firstSegmentNum + segmentCount - 1;
+ for (int j = firstSegmentNum; j <= lastSegmentNum; j++) {
+ addSegment(segments, startUs + index.getTimeUs(j), baseUrl, index.getSegmentUrl(j));
+ }
+ }
+ return segments;
+ }
+
+ /**
+ * Returns DashSegmentIndex for given representation.
+ */
+ private DashSegmentIndex getSegmentIndex(DataSource dataSource, DashManifest manifest,
+ RepresentationKey key) throws IOException, InterruptedException {
+ AdaptationSet adaptationSet = manifest.getPeriod(key.periodIndex).adaptationSets.get(
+ key.adaptationSetIndex);
+ Representation representation = adaptationSet.representations.get(key.representationIndex);
+ DashSegmentIndex index = representation.getIndex();
+ if (index != null) {
+ return index;
+ }
+ ChunkIndex seekMap = DashUtil.loadChunkIndex(dataSource, adaptationSet.type, representation);
+ return seekMap == null ? null : new DashWrappingSegmentIndex(seekMap);
+ }
+
+ private static void addSegment(ArrayList segments, long startTimeUs, String baseUrl,
+ RangedUri rangedUri) {
+ DataSpec dataSpec = new DataSpec(rangedUri.resolveUri(baseUrl), rangedUri.start,
+ rangedUri.length, null);
+ segments.add(new Segment(startTimeUs, dataSpec));
+ }
+
+}
diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadAction.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadAction.java
deleted file mode 100644
index 3c23e25796..0000000000
--- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadAction.java
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * 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 com.google.android.exoplayer2.source.hls.offline;
-
-import android.net.Uri;
-import com.google.android.exoplayer2.offline.DownloadAction;
-import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
-import com.google.android.exoplayer2.offline.SegmentDownloadAction;
-import com.google.android.exoplayer2.util.ClosedSource;
-import java.io.DataInputStream;
-import java.io.DataOutputStream;
-import java.io.IOException;
-
-/** An action to download or remove downloaded HLS streams. */
-@ClosedSource(reason = "Not ready yet")
-public final class HlsDownloadAction extends SegmentDownloadAction {
-
- public static final Deserializer DESERIALIZER = new SegmentDownloadActionDeserializer() {
-
- @Override
- public String getType() {
- return TYPE;
- }
-
- @Override
- protected String readKey(DataInputStream input) throws IOException {
- return input.readUTF();
- }
-
- @Override
- protected String[] createKeyArray(int keyCount) {
- return new String[0];
- }
-
- @Override
- protected DownloadAction createDownloadAction(Uri manifestUri, boolean removeAction,
- String[] keys) {
- return new HlsDownloadAction(manifestUri, removeAction, keys);
- }
-
- };
-
- private static final String TYPE = "HlsDownloadAction";
-
- /** @see SegmentDownloadAction#SegmentDownloadAction(Uri, boolean, Object[]) */
- public HlsDownloadAction(Uri manifestUri, boolean removeAction, String... keys) {
- super(manifestUri, removeAction, keys);
- }
-
- @Override
- public String getType() {
- return TYPE;
- }
-
- @Override
- public HlsDownloader createDownloader(DownloaderConstructorHelper constructorHelper)
- throws IOException {
- HlsDownloader downloader = new HlsDownloader(manifestUri, constructorHelper);
- if (!isRemoveAction()) {
- downloader.selectRepresentations(keys);
- }
- return downloader;
- }
-
- @Override
- protected void writeKey(DataOutputStream output, String key) throws IOException {
- output.writeUTF(key);
- }
-
-}
diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadAction.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadAction.java
deleted file mode 100644
index 7478062ef8..0000000000
--- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadAction.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * 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 com.google.android.exoplayer2.source.smoothstreaming.offline;
-
-import android.net.Uri;
-import com.google.android.exoplayer2.offline.DownloadAction;
-import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
-import com.google.android.exoplayer2.offline.SegmentDownloadAction;
-import com.google.android.exoplayer2.source.smoothstreaming.manifest.TrackKey;
-import com.google.android.exoplayer2.util.ClosedSource;
-import java.io.DataInputStream;
-import java.io.DataOutputStream;
-import java.io.IOException;
-
-/** An action to download or remove downloaded SmoothStreaming streams. */
-@ClosedSource(reason = "Not ready yet")
-public final class SsDownloadAction extends SegmentDownloadAction {
-
- public static final Deserializer DESERIALIZER =
- new SegmentDownloadActionDeserializer() {
-
- @Override
- public String getType() {
- return TYPE;
- }
-
- @Override
- protected TrackKey readKey(DataInputStream input) throws IOException {
- return new TrackKey(input.readInt(), input.readInt());
- }
-
- @Override
- protected TrackKey[] createKeyArray(int keyCount) {
- return new TrackKey[keyCount];
- }
-
- @Override
- protected DownloadAction createDownloadAction(Uri manifestUri, boolean removeAction,
- TrackKey[] keys) {
- return new SsDownloadAction(manifestUri, removeAction, keys);
- }
-
- };
-
- private static final String TYPE = "SsDownloadAction";
-
- /** @see SegmentDownloadAction#SegmentDownloadAction(Uri, boolean, Object[]) */
- public SsDownloadAction(Uri manifestUri, boolean removeAction, TrackKey... keys) {
- super(manifestUri, removeAction, keys);
- }
-
- @Override
- public String getType() {
- return TYPE;
- }
-
- @Override
- public SsDownloader createDownloader(DownloaderConstructorHelper constructorHelper)
- throws IOException {
- SsDownloader downloader = new SsDownloader(manifestUri, constructorHelper);
- if (!isRemoveAction()) {
- downloader.selectRepresentations(keys);
- }
- return downloader;
- }
-
- @Override
- protected void writeKey(DataOutputStream output, TrackKey key) throws IOException {
- output.writeInt(key.streamElementIndex);
- output.writeInt(key.trackIndex);
- }
-
-}