From 103c3b631b1e8cba0b6d9d5fc959c73c5f66de22 Mon Sep 17 00:00:00 2001 From: eguven Date: Tue, 28 Mar 2017 09:06:21 -0700 Subject: [PATCH] Add DashDownloader helper class to download dash streams ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=151456161 --- .../java/com/google/android/exoplayer2/C.java | 9 + .../upstream/cache/CacheDataSource.java | 11 +- .../exoplayer2/util/PriorityTaskManager.java | 2 +- .../dash/offline/DashDownloaderTest.java | 420 ++++++++++++++++++ .../exoplayer2/source/dash/DashUtil.java | 6 +- .../source/dash/DashWrappingSegmentIndex.java | 2 +- .../{DashTest.java => DashStreamingTest.java} | 6 +- .../playbacktests/gts/DashTestData.java | 2 +- 8 files changed, 448 insertions(+), 10 deletions(-) create mode 100644 library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java rename playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/{DashTest.java => DashStreamingTest.java} (99%) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/C.java b/library/core/src/main/java/com/google/android/exoplayer2/C.java index 29f8220037..02e5939b86 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/C.java @@ -541,9 +541,18 @@ public final class C { /** * Priority for media playback. + * + *

Larger values indicate higher priorities. */ public static final int PRIORITY_PLAYBACK = 0; + /** + * Priority for media downloading. + * + *

Larger values indicate higher priorities. + */ + public static final int PRIORITY_DOWNLOAD = PRIORITY_PLAYBACK - 1000; + /** * Converts a time in microseconds to the corresponding time in milliseconds, preserving * {@link #TIME_UNSET} values. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java index a11f1956ea..26c77b5dd1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java @@ -108,6 +108,15 @@ public final class CacheDataSource implements DataSource { private boolean currentRequestIgnoresCache; private long totalCachedBytesRead; + /** + * Generates a cache key out of the given {@link Uri}. + * + * @param uri Uri of a content which the requested key is for. + */ + public static String generateKey(Uri uri) { + return uri.toString(); + } + /** * Constructs an instance with default {@link DataSource} and {@link DataSink} instances for * reading and writing the cache and with {@link #DEFAULT_MAX_CACHE_FILE_SIZE}. @@ -171,7 +180,7 @@ public final class CacheDataSource implements DataSource { try { uri = dataSpec.uri; flags = dataSpec.flags; - key = dataSpec.key != null ? dataSpec.key : uri.toString(); + key = dataSpec.key != null ? dataSpec.key : generateKey(dataSpec.uri); readPosition = dataSpec.position; currentRequestIgnoresCache = (ignoreCacheOnError && seenCacheError) || (dataSpec.length == C.LENGTH_UNSET && ignoreCacheForUnsetLengthRequests); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/PriorityTaskManager.java b/library/core/src/main/java/com/google/android/exoplayer2/util/PriorityTaskManager.java index fb61d3ba4a..2516b538c6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/PriorityTaskManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/PriorityTaskManager.java @@ -54,7 +54,7 @@ public final class PriorityTaskManager { /** * Register a new task. The task must call {@link #remove(int)} when done. * - * @param priority The priority of the task. + * @param priority The priority of the task. Larger values indicate higher priorities. */ public void add(int priority) { synchronized (lock) { 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..c2578c196f --- /dev/null +++ b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java @@ -0,0 +1,420 @@ +/* + * 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 android.test.InstrumentationTestCase; +import android.test.MoreAsserts; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.source.dash.manifest.DashManifest; +import com.google.android.exoplayer2.source.dash.manifest.RepresentationKey; +import com.google.android.exoplayer2.source.dash.offline.DashDownloader.ProgressListener; +import com.google.android.exoplayer2.testutil.FakeDataSource; +import com.google.android.exoplayer2.testutil.FakeDataSource.FakeData; +import com.google.android.exoplayer2.testutil.FakeDataSource.FakeDataSet; +import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.upstream.DataSourceInputStream; +import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.DummyDataSource; +import com.google.android.exoplayer2.upstream.cache.CacheDataSource; +import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor; +import com.google.android.exoplayer2.upstream.cache.SimpleCache; +import com.google.android.exoplayer2.util.ClosedSource; +import com.google.android.exoplayer2.util.Util; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.util.Arrays; + +/** + * Unit tests for {@link DashDownloader}. + */ +@ClosedSource(reason = "Not ready yet") +public class DashDownloaderTest extends InstrumentationTestCase { + + private static final 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" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "").getBytes(); + + private static final byte[] TEST_MPD_NO_INDEX = + ("\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "").getBytes(); + + private File tempFolder; + private SimpleCache cache; + + @Override + public void setUp() throws Exception { + super.setUp(); + tempFolder = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); + cache = new SimpleCache(tempFolder, new NoOpCacheEvictor()); + } + + @Override + public void tearDown() throws Exception { + Util.recursiveDelete(tempFolder); + } + + public void testDownloadManifest() throws Exception { + FakeDataSet fakeDataSet = new FakeDataSet() + .setData("test.mpd", TEST_MPD); + DashDownloader dashDownloader = + new DashDownloader("test.mpd", cache, new FakeDataSource(fakeDataSet)); + + DashManifest manifest = dashDownloader.downloadManifest(); + + assertNotNull(manifest); + assertCachedData(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") + .appendReadData(testMpdFirstPart) + .appendReadError(new IOException()) + .appendReadData(testMpdSecondPart) + .endData(); + DashDownloader dashDownloader = + new DashDownloader("test.mpd", cache, new FakeDataSource(fakeDataSet)); + + // downloadManifest fails on the first try + try { + dashDownloader.downloadManifest(); + fail(); + } catch (IOException e) { + // ignore + } + assertCachedData("test.mpd", testMpdFirstPart); + + // on the second try it downloads the rest of the data + DashManifest manifest = dashDownloader.downloadManifest(); + + assertNotNull(manifest); + assertCachedData(fakeDataSet); + } + + public void testDownloadRepresentation() throws Exception { + FakeDataSet fakeDataSet = new FakeDataSet() + .setData("test.mpd", TEST_MPD) + .setData("audio_init_data", TestUtil.buildTestData(10)) + .setData("audio_segment_1", TestUtil.buildTestData(4)) + .setData("audio_segment_2", TestUtil.buildTestData(5)) + .setData("audio_segment_3", TestUtil.buildTestData(6)); + DashDownloader dashDownloader = + new DashDownloader("test.mpd", cache, new FakeDataSource(fakeDataSet)); + dashDownloader.downloadManifest(); + + dashDownloader.selectRepresentations(new RepresentationKey(0, 0, 0)); + dashDownloader.downloadRepresentations(null); + + assertCachedData(fakeDataSet); + } + + public void testDownloadRepresentationInSmallParts() throws Exception { + FakeDataSet fakeDataSet = new FakeDataSet() + .setData("test.mpd", TEST_MPD) + .setData("audio_init_data", TestUtil.buildTestData(10)) + .newData("audio_segment_1") + .appendReadData(TestUtil.buildTestData(10)) + .appendReadData(TestUtil.buildTestData(10)) + .appendReadData(TestUtil.buildTestData(10)) + .endData() + .setData("audio_segment_2", TestUtil.buildTestData(5)) + .setData("audio_segment_3", TestUtil.buildTestData(6)); + DashDownloader dashDownloader = + new DashDownloader("test.mpd", cache, new FakeDataSource(fakeDataSet)); + dashDownloader.downloadManifest(); + + dashDownloader.selectRepresentations(new RepresentationKey(0, 0, 0)); + dashDownloader.downloadRepresentations(null); + + assertCachedData(fakeDataSet); + } + + public void testDownloadRepresentations() throws Exception { + FakeDataSet fakeDataSet = new FakeDataSet() + .setData("test.mpd", TEST_MPD) + .setData("audio_init_data", TestUtil.buildTestData(10)) + .setData("audio_segment_1", TestUtil.buildTestData(4)) + .setData("audio_segment_2", TestUtil.buildTestData(5)) + .setData("audio_segment_3", TestUtil.buildTestData(6)) + .setData("text_segment_1", TestUtil.buildTestData(1)) + .setData("text_segment_2", TestUtil.buildTestData(2)) + .setData("text_segment_3", TestUtil.buildTestData(3)); + DashDownloader dashDownloader = + new DashDownloader("test.mpd", cache, new FakeDataSource(fakeDataSet)); + dashDownloader.downloadManifest(); + + dashDownloader.selectRepresentations( + new RepresentationKey(0, 0, 0), + new RepresentationKey(0, 1, 0)); + dashDownloader.downloadRepresentations(null); + + assertCachedData(fakeDataSet); + } + + public void testDownloadRepresentationFailure() throws Exception { + FakeDataSet fakeDataSet = new FakeDataSet() + .setData("test.mpd", TEST_MPD) + .setData("audio_init_data", TestUtil.buildTestData(10)) + .setData("audio_segment_1", TestUtil.buildTestData(4)) + .newData("audio_segment_2") + .appendReadData(TestUtil.buildTestData(2)) + .appendReadError(new IOException()) + .appendReadData(TestUtil.buildTestData(3)) + .endData() + .setData("audio_segment_3", TestUtil.buildTestData(6)); + DashDownloader dashDownloader = + new DashDownloader("test.mpd", cache, new FakeDataSource(fakeDataSet)); + dashDownloader.downloadManifest(); + + dashDownloader.selectRepresentations(new RepresentationKey(0, 0, 0)); + // downloadRepresentations fails on the first try + try { + dashDownloader.downloadRepresentations(null); + fail(); + } catch (IOException e) { + // ignore + } + dashDownloader.downloadRepresentations(null); + + assertCachedData(fakeDataSet); + } + + public void testCounters() throws Exception { + FakeDataSet fakeDataSet = new FakeDataSet() + .setData("test.mpd", TEST_MPD) + .setData("audio_init_data", TestUtil.buildTestData(10)) + .setData("audio_segment_1", TestUtil.buildTestData(4)) + .newData("audio_segment_2") + .appendReadData(TestUtil.buildTestData(2)) + .appendReadError(new IOException()) + .appendReadData(TestUtil.buildTestData(3)) + .endData() + .setData("audio_segment_3", TestUtil.buildTestData(6)); + DashDownloader dashDownloader = + new DashDownloader("test.mpd", cache, new FakeDataSource(fakeDataSet)); + dashDownloader.downloadManifest(); + + assertCounters(dashDownloader, C.LENGTH_UNSET, C.LENGTH_UNSET, C.LENGTH_UNSET); + + dashDownloader.selectRepresentations(new RepresentationKey(0, 0, 0)); + dashDownloader.initStatus(); + assertCounters(dashDownloader, 3, 0, 0); + + // downloadRepresentations fails after downloading init data, segment 1 and 2 bytes in segment 2 + try { + dashDownloader.downloadRepresentations(null); + fail(); + } catch (IOException e) { + // ignore + } + dashDownloader.initStatus(); + assertCounters(dashDownloader, 3, 1, 10 + 4 + 2); + + dashDownloader.downloadRepresentations(null); + + assertCounters(dashDownloader, 3, 3, 10 + 4 + 5 + 6); + } + + public void testListener() throws Exception { + FakeDataSet fakeDataSet = new FakeDataSet() + .setData("test.mpd", TEST_MPD) + .setData("audio_init_data", TestUtil.buildTestData(10)) + .setData("audio_segment_1", TestUtil.buildTestData(4)) + .setData("audio_segment_2", TestUtil.buildTestData(5)) + .setData("audio_segment_3", TestUtil.buildTestData(6)); + DashDownloader dashDownloader = + new DashDownloader("test.mpd", cache, new FakeDataSource(fakeDataSet)); + dashDownloader.downloadManifest(); + + dashDownloader.selectRepresentations(new RepresentationKey(0, 0, 0)); + dashDownloader.downloadRepresentations(new ProgressListener() { + private int counter = 0; + @Override + public void onDownloadProgress(DashDownloader dashDownloader, int totalSegments, + int downloadedSegments, + long downloadedBytes) { + switch (counter++) { + case 0: + assertTrue(totalSegments == 3 && downloadedSegments == 0 && downloadedBytes == 10); + break; + case 1: + assertTrue(totalSegments == 3 && downloadedSegments == 1 && downloadedBytes == 14); + break; + case 2: + assertTrue(totalSegments == 3 && downloadedSegments == 2 && downloadedBytes == 19); + break; + case 3: + assertTrue(totalSegments == 3 && downloadedSegments == 3 && downloadedBytes == 25); + break; + default: + fail(); + } + } + }); + } + + public void testRemoveAll() throws Exception { + FakeDataSet fakeDataSet = new FakeDataSet() + .setData("test.mpd", TEST_MPD) + .setData("audio_init_data", TestUtil.buildTestData(10)) + .setData("audio_segment_1", TestUtil.buildTestData(4)) + .setData("audio_segment_2", TestUtil.buildTestData(5)) + .setData("audio_segment_3", TestUtil.buildTestData(6)) + .setData("text_segment_1", TestUtil.buildTestData(1)) + .setData("text_segment_2", TestUtil.buildTestData(2)) + .setData("text_segment_3", TestUtil.buildTestData(3)); + DashDownloader dashDownloader = + new DashDownloader("test.mpd", cache, new FakeDataSource(fakeDataSet)); + dashDownloader.downloadManifest(); + dashDownloader.selectRepresentations( + new RepresentationKey(0, 0, 0), + new RepresentationKey(0, 1, 0)); + dashDownloader.downloadRepresentations(null); + + dashDownloader.removeAll(); + + assertEquals(0, cache.getCacheSpace()); + } + + public void testRemoveRepresentations() throws Exception { + FakeDataSet fakeDataSet = new FakeDataSet() + .setData("test.mpd", TEST_MPD) + .setData("audio_init_data", TestUtil.buildTestData(10)) + .setData("audio_segment_1", TestUtil.buildTestData(4)) + .setData("audio_segment_2", TestUtil.buildTestData(5)) + .setData("audio_segment_3", TestUtil.buildTestData(6)) + .setData("text_segment_1", TestUtil.buildTestData(1)) + .setData("text_segment_2", TestUtil.buildTestData(2)) + .setData("text_segment_3", TestUtil.buildTestData(3)); + DashDownloader dashDownloader = + new DashDownloader("test.mpd", cache, new FakeDataSource(fakeDataSet)); + dashDownloader.downloadManifest(); + dashDownloader.selectRepresentations( + new RepresentationKey(0, 0, 0), + new RepresentationKey(0, 1, 0)); + dashDownloader.downloadRepresentations(null); + + dashDownloader.removeRepresentations(); + + assertEquals(TEST_MPD.length, cache.getCacheSpace()); + assertCachedData("test.mpd", TEST_MPD); + } + + public void testMpdNoIndex() throws Exception { + FakeDataSet fakeDataSet = new FakeDataSet() + .setData("test.mpd", TEST_MPD_NO_INDEX) + .setData("test_segment_1", TestUtil.buildTestData(4)); + DashDownloader dashDownloader = + new DashDownloader("test.mpd", cache, new FakeDataSource(fakeDataSet)); + dashDownloader.downloadManifest(); + + dashDownloader.selectRepresentations(new RepresentationKey(0, 0, 0)); + dashDownloader.initStatus(); + try { + dashDownloader.downloadRepresentations(null); + fail(); + } catch (DashDownloaderException e) { + // expected interrupt. + } + dashDownloader.removeAll(); + + assertEquals(0, cache.getCacheSpace()); + } + + private void assertCachedData(FakeDataSet fakeDataSet) throws IOException { + int totalLength = 0; + for (FakeData fakeData : fakeDataSet.getAllData()) { + byte[] data = fakeData.getData(); + assertCachedData(fakeData.uri, data); + totalLength += data.length; + } + assertEquals(totalLength, cache.getCacheSpace()); + } + + private void assertCachedData(String uriString, byte[] expected) throws IOException { + CacheDataSource dataSource = new CacheDataSource(cache, DummyDataSource.INSTANCE, 0); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + DataSourceInputStream inputStream = new DataSourceInputStream(dataSource, + new DataSpec(Uri.parse(uriString), DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH)); + try { + inputStream.open(); + byte[] buffer = new byte[1024]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + } + } catch (IOException e) { + // Ignore + } finally { + inputStream.close(); + } + MoreAsserts.assertEquals(expected, outputStream.toByteArray()); + } + + 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/DashUtil.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashUtil.java index 3ec44c2f69..2febeb8c81 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashUtil.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashUtil.java @@ -48,14 +48,14 @@ public final class DashUtil { * Loads a DASH manifest. * * @param dataSource The {@link HttpDataSource} from which the manifest should be read. - * @param manifestUriString The URI of the manifest to be read. + * @param manifestUri The URI of the manifest to be read. * @return An instance of {@link DashManifest}. * @throws IOException Thrown when there is an error while loading. */ - public static DashManifest loadManifest(DataSource dataSource, String manifestUriString) + public static DashManifest loadManifest(DataSource dataSource, String manifestUri) throws IOException { DataSourceInputStream inputStream = new DataSourceInputStream(dataSource, - new DataSpec(Uri.parse(manifestUriString), DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH)); + new DataSpec(Uri.parse(manifestUri), DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH)); try { inputStream.open(); DashManifestParser parser = new DashManifestParser(); diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashWrappingSegmentIndex.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashWrappingSegmentIndex.java index 40f3448f6a..8cd5018dc7 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashWrappingSegmentIndex.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashWrappingSegmentIndex.java @@ -22,7 +22,7 @@ import com.google.android.exoplayer2.source.dash.manifest.RangedUri; * An implementation of {@link DashSegmentIndex} that wraps a {@link ChunkIndex} parsed from a * media stream. */ -/* package */ final class DashWrappingSegmentIndex implements DashSegmentIndex { +public final class DashWrappingSegmentIndex implements DashSegmentIndex { private final ChunkIndex chunkIndex; diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashStreamingTest.java similarity index 99% rename from playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java rename to playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashStreamingTest.java index fc0701da8d..e7441362cf 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashStreamingTest.java @@ -28,9 +28,9 @@ import com.google.android.exoplayer2.util.Util; /** * Tests DASH playbacks using {@link ExoPlayer}. */ -public final class DashTest extends ActivityInstrumentationTestCase2 { +public final class DashStreamingTest extends ActivityInstrumentationTestCase2 { - private static final String TAG = "DashTest"; + private static final String TAG = "DashStreamingTest"; private static final ActionSchedule SEEKING_SCHEDULE = new ActionSchedule.Builder(TAG) .delay(10000).seek(15000) @@ -72,7 +72,7 @@ public final class DashTest extends ActivityInstrumentationTestCase2