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); - } - -}