Fix download percentage reporting

- When calculating the downloaded percentage in DASH, there was no
  way to disambiguate between 0 of 0 segments being downloaded because
  there are no cached indexes (i.e. 0% downloaded) and 0 of 0 segments
  being downloaded because the index defines 0 segments (i.e. 100%
  downloaded).
- Also replace use of NaN with a named constant.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=194453202
This commit is contained in:
olly 2018-04-26 14:21:06 -07:00 committed by Oliver Woodman
parent fe32401792
commit 3e76464666
12 changed files with 85 additions and 142 deletions

View File

@ -204,7 +204,8 @@ public class DownloadActivity extends Activity {
public RepresentationItem(Parcelable key, String title, float percentDownloaded) { public RepresentationItem(Parcelable key, String title, float percentDownloaded) {
this.key = key; this.key = key;
this.title = title; this.title = title;
this.percentDownloaded = (int) percentDownloaded; this.percentDownloaded =
(int) (percentDownloaded == C.PERCENTAGE_UNSET ? 0 : percentDownloaded);
} }
@Override @Override

View File

@ -64,6 +64,9 @@ public final class C {
*/ */
public static final int LENGTH_UNSET = -1; public static final int LENGTH_UNSET = -1;
/** Represents an unset or unknown percentage. */
public static final int PERCENTAGE_UNSET = -1;
/** /**
* The number of microseconds in one second. * The number of microseconds in one second.
*/ */

View File

@ -541,14 +541,11 @@ public final class DownloadManager {
/** The state of the task. See {@link State}. */ /** The state of the task. See {@link State}. */
public final @State int state; public final @State int state;
/** /**
* The download percentage, or {@link Float#NaN} if it can't be calculated or the task is for * The estimated download percentage, or {@link C#PERCENTAGE_UNSET} if no estimate is available
* removing. * or if this is a removal task.
*/ */
public final float downloadPercentage; public final float downloadPercentage;
/** /** The total number of downloaded bytes. */
* The downloaded bytes, or {@link C#LENGTH_UNSET} if it hasn't been calculated yet or the task
* is for removing.
*/
public final long downloadedBytes; public final long downloadedBytes;
/** If {@link #state} is {@link #STATE_ERROR} then this is the cause, otherwise null. */ /** If {@link #state} is {@link #STATE_ERROR} then this is the cause, otherwise null. */
public final Throwable error; public final Throwable error;
@ -648,19 +645,16 @@ public final class DownloadManager {
} }
/** /**
* Returns the download percentage, or {@link Float#NaN} if it can't be calculated yet. This * Returns the estimated download percentage, or {@link C#PERCENTAGE_UNSET} if no estimate is
* value can be an estimation. * available.
*/ */
public float getDownloadPercentage() { public float getDownloadPercentage() {
return downloader != null ? downloader.getDownloadPercentage() : Float.NaN; return downloader != null ? downloader.getDownloadPercentage() : C.PERCENTAGE_UNSET;
} }
/** /** Returns the total number of downloaded bytes. */
* Returns the total number of downloaded bytes, or {@link C#LENGTH_UNSET} if it hasn't been
* calculated yet.
*/
public long getDownloadedBytes() { public long getDownloadedBytes() {
return downloader != null ? downloader.getDownloadedBytes() : C.LENGTH_UNSET; return downloader != null ? downloader.getDownloadedBytes() : 0;
} }
@Override @Override

View File

@ -29,8 +29,6 @@ public interface Downloader {
* @throws DownloadException Thrown if the media cannot be downloaded. * @throws DownloadException Thrown if the media cannot be downloaded.
* @throws InterruptedException If the thread has been interrupted. * @throws InterruptedException If the thread has been interrupted.
* @throws IOException Thrown when there is an io error while reading from cache. * @throws IOException Thrown when there is an io error while reading from cache.
* @see #getDownloadedBytes()
* @see #getDownloadPercentage()
*/ */
void init() throws InterruptedException, IOException; void init() throws InterruptedException, IOException;
@ -50,20 +48,12 @@ public interface Downloader {
*/ */
void remove() throws InterruptedException; void remove() throws InterruptedException;
/** /** Returns the total number of downloaded bytes. */
* Returns the total number of downloaded bytes, or {@link C#LENGTH_UNSET} if it hasn't been
* calculated yet.
*
* @see #init()
*/
long getDownloadedBytes(); long getDownloadedBytes();
/** /**
* Returns the download percentage, or {@link Float#NaN} if it can't be calculated yet. This * Returns the estimated download percentage, or {@link C#PERCENTAGE_UNSET} if no estimate is
* value can be an estimation. * available.
*
* @see #init()
*/ */
float getDownloadPercentage(); float getDownloadPercentage();
} }

View File

@ -83,7 +83,8 @@ public final class ProgressiveDownloader implements Downloader {
@Override @Override
public float getDownloadPercentage() { public float getDownloadPercentage() {
long contentLength = cachingCounters.contentLength; long contentLength = cachingCounters.contentLength;
return contentLength == C.LENGTH_UNSET ? Float.NaN return contentLength == C.LENGTH_UNSET
? C.PERCENTAGE_UNSET
: ((cachingCounters.totalCachedBytes() * 100f) / contentLength); : ((cachingCounters.totalCachedBytes() * 100f) / contentLength);
} }

View File

@ -17,6 +17,7 @@ package com.google.android.exoplayer2.offline;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.util.Pair;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.DataSpec;
@ -33,9 +34,6 @@ import java.util.List;
/** /**
* Base class for multi segment stream downloaders. * Base class for multi segment stream downloaders.
* *
* <p>All of the methods are blocking. Also they are not thread safe, except {@link
* #getTotalSegments()}, {@link #getDownloadedSegments()} and {@link #getDownloadedBytes()}.
*
* @param <M> The type of the manifest object. * @param <M> The type of the manifest object.
* @param <K> The type of the representation key object. * @param <K> The type of the representation key object.
*/ */
@ -101,6 +99,9 @@ public abstract class SegmentDownloader<M extends FilterableManifest<M, K>, K>
return getManifestIfNeeded(false); return getManifestIfNeeded(false);
} }
/** Returns keys for all representations. */
public abstract K[] getAllRepresentationKeys() throws IOException;
/** /**
* Selects multiple representations pointed to by the keys for downloading, checking status. Any * Selects multiple representations pointed to by the keys for downloading, checking status. Any
* previous selection is cleared. If keys array is null or empty then all representations are * previous selection is cleared. If keys array is null or empty then all representations are
@ -113,25 +114,13 @@ public abstract class SegmentDownloader<M extends FilterableManifest<M, K>, K>
} }
/** /**
* Returns keys for all representations. * Initializes the downloader for the selected representations.
* *
* @see #selectRepresentations(Object[]) * @throws IOException Thrown when there is an error downloading.
*/
public abstract K[] getAllRepresentationKeys() throws IOException;
/**
* Initializes the total segments, downloaded segments and downloaded bytes counters for the
* selected representations.
*
* @throws IOException Thrown when there is an io error while reading from cache.
* @throws DownloadException Thrown if the media cannot be downloaded.
* @throws InterruptedException If the thread has been interrupted. * @throws InterruptedException If the thread has been interrupted.
* @see #getTotalSegments()
* @see #getDownloadedSegments()
* @see #getDownloadedBytes()
*/ */
@Override @Override
public final void init() throws InterruptedException, IOException { public final void init() throws IOException, InterruptedException {
try { try {
getManifestIfNeeded(true); getManifestIfNeeded(true);
} catch (IOException e) { } catch (IOException e) {
@ -140,7 +129,7 @@ public abstract class SegmentDownloader<M extends FilterableManifest<M, K>, K>
} }
try { try {
initStatus(true); initStatus(true);
} catch (IOException | InterruptedException e) { } catch (IOException e) {
resetCounters(); resetCounters();
throw e; throw e;
} }
@ -150,8 +139,7 @@ public abstract class SegmentDownloader<M extends FilterableManifest<M, K>, K>
* Downloads the content for the selected representations in sync or resumes a previously stopped * Downloads the content for the selected representations in sync or resumes a previously stopped
* download. * download.
* *
* @throws IOException Thrown when there is an io error while downloading. * @throws IOException Thrown when there is an error downloading.
* @throws DownloadException Thrown if the media cannot be downloaded.
* @throws InterruptedException If the thread has been interrupted. * @throws InterruptedException If the thread has been interrupted.
*/ */
@Override @Override
@ -174,32 +162,6 @@ public abstract class SegmentDownloader<M extends FilterableManifest<M, K>, K>
} }
} }
/**
* Returns the total number of segments in the representations which are selected, or {@link
* C#LENGTH_UNSET} if it hasn't been calculated yet.
*
* @see #init()
*/
public final int getTotalSegments() {
return totalSegments;
}
/**
* Returns the total number of downloaded segments in the representations which are selected, or
* {@link C#LENGTH_UNSET} if it hasn't been calculated yet.
*
* @see #init()
*/
public final int getDownloadedSegments() {
return downloadedSegments;
}
/**
* Returns the total number of downloaded bytes in the representations which are selected, or
* {@link C#LENGTH_UNSET} if it hasn't been calculated yet.
*
* @see #init()
*/
@Override @Override
public final long getDownloadedBytes() { public final long getDownloadedBytes() {
return downloadedBytes; return downloadedBytes;
@ -211,7 +173,7 @@ public abstract class SegmentDownloader<M extends FilterableManifest<M, K>, K>
int totalSegments = this.totalSegments; int totalSegments = this.totalSegments;
int downloadedSegments = this.downloadedSegments; int downloadedSegments = this.downloadedSegments;
if (totalSegments == C.LENGTH_UNSET || downloadedSegments == C.LENGTH_UNSET) { if (totalSegments == C.LENGTH_UNSET || downloadedSegments == C.LENGTH_UNSET) {
return Float.NaN; return C.PERCENTAGE_UNSET;
} }
return totalSegments == 0 ? 100f : (downloadedSegments * 100f) / totalSegments; return totalSegments == 0 ? 100f : (downloadedSegments * 100f) / totalSegments;
} }
@ -228,7 +190,7 @@ public abstract class SegmentDownloader<M extends FilterableManifest<M, K>, K>
if (manifest != null) { if (manifest != null) {
List<Segment> segments = null; List<Segment> segments = null;
try { try {
segments = getSegments(offlineDataSource, manifest, true); segments = getSegments(offlineDataSource, manifest, true).first;
} catch (IOException e) { } catch (IOException e) {
// Ignore exceptions. We do our best with what's available offline. // Ignore exceptions. We do our best with what's available offline.
} }
@ -263,16 +225,17 @@ public abstract class SegmentDownloader<M extends FilterableManifest<M, K>, K>
* @throws InterruptedException Thrown if the thread was interrupted. * @throws InterruptedException Thrown if the thread was interrupted.
* @throws IOException Thrown if {@code allowPartialIndex} is false and a load error occurs, or if * @throws IOException Thrown if {@code allowPartialIndex} is false and a load error occurs, or if
* the media is not in a form that allows for its segments to be listed. * the media is not in a form that allows for its segments to be listed.
* @return A list of {@link Segment}s for given keys. * @return A list of {@link Segment}s for given keys, and a boolean indicating whether the list is
* complete.
*/ */
protected abstract List<Segment> getSegments( protected abstract Pair<List<Segment>, Boolean> getSegments(
DataSource dataSource, M manifest, boolean allowIncompleteIndex) DataSource dataSource, M manifest, boolean allowIncompleteIndex)
throws InterruptedException, IOException; throws InterruptedException, IOException;
private void resetCounters() { private void resetCounters() {
totalSegments = C.LENGTH_UNSET; totalSegments = C.LENGTH_UNSET;
downloadedSegments = C.LENGTH_UNSET; downloadedSegments = 0;
downloadedBytes = C.LENGTH_UNSET; downloadedBytes = 0;
} }
private void remove(Uri uri) { private void remove(Uri uri) {
@ -289,9 +252,11 @@ public abstract class SegmentDownloader<M extends FilterableManifest<M, K>, K>
throws IOException, InterruptedException { throws IOException, InterruptedException {
DataSource dataSource = getDataSource(offline); DataSource dataSource = getDataSource(offline);
M filteredManifest = keys.isEmpty() ? manifest : manifest.copy(keys); M filteredManifest = keys.isEmpty() ? manifest : manifest.copy(keys);
List<Segment> segments = getSegments(dataSource, filteredManifest, offline); Pair<List<Segment>, Boolean> result = getSegments(dataSource, filteredManifest, offline);
List<Segment> segments = result.first;
boolean isSegmentListComplete = result.second;
CachingCounters cachingCounters = new CachingCounters(); CachingCounters cachingCounters = new CachingCounters();
totalSegments = segments.size(); totalSegments = isSegmentListComplete ? segments.size() : C.LENGTH_UNSET;
downloadedSegments = 0; downloadedSegments = 0;
downloadedBytes = 0; downloadedBytes = 0;
for (int i = segments.size() - 1; i >= 0; i--) { for (int i = segments.size() - 1; i >= 0; i--) {

View File

@ -16,6 +16,7 @@
package com.google.android.exoplayer2.source.dash.offline; package com.google.android.exoplayer2.source.dash.offline;
import android.net.Uri; import android.net.Uri;
import android.util.Pair;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.ChunkIndex; import com.google.android.exoplayer2.extractor.ChunkIndex;
import com.google.android.exoplayer2.offline.DownloadException; import com.google.android.exoplayer2.offline.DownloadException;
@ -39,9 +40,6 @@ import java.util.List;
/** /**
* Helper class to download DASH streams. * Helper class to download DASH streams.
* *
* <p>Except {@link #getTotalSegments()}, {@link #getDownloadedSegments()} and {@link
* #getDownloadedBytes()}, this class isn't thread safe.
*
* <p>Example usage: * <p>Example usage:
* *
* <pre>{@code * <pre>{@code
@ -89,29 +87,32 @@ public final class DashDownloader extends SegmentDownloader<DashManifest, Repres
} }
@Override @Override
protected List<Segment> getSegments( protected Pair<List<Segment>, Boolean> getSegments(
DataSource dataSource, DashManifest manifest, boolean allowIndexLoadErrors) DataSource dataSource, DashManifest manifest, boolean allowIndexLoadErrors)
throws InterruptedException, IOException { throws InterruptedException, IOException {
ArrayList<Segment> segments = new ArrayList<>(); ArrayList<Segment> segments = new ArrayList<>();
boolean segmentListComplete = true;
for (int i = 0; i < manifest.getPeriodCount(); i++) { for (int i = 0; i < manifest.getPeriodCount(); i++) {
Period period = manifest.getPeriod(i); Period period = manifest.getPeriod(i);
long periodStartUs = C.msToUs(period.startMs); long periodStartUs = C.msToUs(period.startMs);
long periodDurationUs = manifest.getPeriodDurationUs(i); long periodDurationUs = manifest.getPeriodDurationUs(i);
List<AdaptationSet> adaptationSets = period.adaptationSets; List<AdaptationSet> adaptationSets = period.adaptationSets;
for (int j = 0; j < adaptationSets.size(); j++) { for (int j = 0; j < adaptationSets.size(); j++) {
addSegmentsForAdaptationSet( if (!addSegmentsForAdaptationSet(
dataSource, dataSource,
adaptationSets.get(j), adaptationSets.get(j),
periodStartUs, periodStartUs,
periodDurationUs, periodDurationUs,
allowIndexLoadErrors, allowIndexLoadErrors,
segments); segments)) {
segmentListComplete = false;
} }
} }
return segments; }
return Pair.<List<Segment>, Boolean>create(segments, segmentListComplete);
} }
private static void addSegmentsForAdaptationSet( private static boolean addSegmentsForAdaptationSet(
DataSource dataSource, DataSource dataSource,
AdaptationSet adaptationSet, AdaptationSet adaptationSet,
long periodStartUs, long periodStartUs,
@ -119,6 +120,7 @@ public final class DashDownloader extends SegmentDownloader<DashManifest, Repres
boolean allowIndexLoadErrors, boolean allowIndexLoadErrors,
ArrayList<Segment> out) ArrayList<Segment> out)
throws IOException, InterruptedException { throws IOException, InterruptedException {
boolean segmentListComplete = true;
for (int i = 0; i < adaptationSet.representations.size(); i++) { for (int i = 0; i < adaptationSet.representations.size(); i++) {
Representation representation = adaptationSet.representations.get(i); Representation representation = adaptationSet.representations.get(i);
DashSegmentIndex index; DashSegmentIndex index;
@ -129,12 +131,12 @@ public final class DashDownloader extends SegmentDownloader<DashManifest, Repres
throw new DownloadException("Missing segment index"); throw new DownloadException("Missing segment index");
} }
} catch (IOException e) { } catch (IOException e) {
if (allowIndexLoadErrors) { if (!allowIndexLoadErrors) {
// Loading failed, but load errors are allowed. Advance to the next representation.
continue;
} else {
throw e; throw e;
} }
// Loading failed, but load errors are allowed. Advance to the next representation.
segmentListComplete = false;
continue;
} }
int segmentCount = index.getSegmentCount(periodDurationUs); int segmentCount = index.getSegmentCount(periodDurationUs);
@ -157,6 +159,8 @@ public final class DashDownloader extends SegmentDownloader<DashManifest, Repres
addSegment(periodStartUs + index.getTimeUs(j), baseUrl, index.getSegmentUrl(j), out); addSegment(periodStartUs + index.getTimeUs(j), baseUrl, index.getSegmentUrl(j), out);
} }
} }
return segmentListComplete;
} }
private static void addSegment( private static void addSegment(

View File

@ -26,7 +26,6 @@ import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.offline.DownloadException; import com.google.android.exoplayer2.offline.DownloadException;
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
import com.google.android.exoplayer2.source.dash.manifest.DashManifest; import com.google.android.exoplayer2.source.dash.manifest.DashManifest;
@ -65,7 +64,7 @@ public class DashDownloaderTest {
} }
@After @After
public void tearDown() throws Exception { public void tearDown() {
Util.recursiveDelete(tempFolder); Util.recursiveDelete(tempFolder);
} }
@ -311,11 +310,11 @@ public class DashDownloaderTest {
.setRandomData("audio_segment_3", 6); .setRandomData("audio_segment_3", 6);
DashDownloader dashDownloader = getDashDownloader(fakeDataSet); DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
assertCounters(dashDownloader, C.LENGTH_UNSET, C.LENGTH_UNSET, C.LENGTH_UNSET); assertThat(dashDownloader.getDownloadedBytes()).isEqualTo(0);
dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)}); dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)});
dashDownloader.init(); dashDownloader.init();
assertCounters(dashDownloader, C.LENGTH_UNSET, C.LENGTH_UNSET, C.LENGTH_UNSET); assertThat(dashDownloader.getDownloadedBytes()).isEqualTo(0);
// downloadRepresentations fails after downloading init data, segment 1 and 2 bytes in segment 2 // downloadRepresentations fails after downloading init data, segment 1 and 2 bytes in segment 2
try { try {
@ -325,11 +324,10 @@ public class DashDownloaderTest {
// ignore // ignore
} }
dashDownloader.init(); dashDownloader.init();
assertCounters(dashDownloader, 4, 2, 10 + 4 + 2); assertThat(dashDownloader.getDownloadedBytes()).isEqualTo(10 + 4 + 2);
dashDownloader.download(); dashDownloader.download();
assertThat(dashDownloader.getDownloadedBytes()).isEqualTo(10 + 4 + 5 + 6);
assertCounters(dashDownloader, 4, 4, 10 + 4 + 5 + 6);
} }
@Test @Test
@ -399,13 +397,4 @@ public class DashDownloaderTest {
return new DashDownloader(TEST_MPD_URI, new DownloaderConstructorHelper(cache, factory)); return new DashDownloader(TEST_MPD_URI, new DownloaderConstructorHelper(cache, factory));
} }
private static void assertCounters(
DashDownloader dashDownloader,
int totalSegments,
int downloadedSegments,
int downloadedBytes) {
assertThat(dashDownloader.getTotalSegments()).isEqualTo(totalSegments);
assertThat(dashDownloader.getDownloadedSegments()).isEqualTo(downloadedSegments);
assertThat(dashDownloader.getDownloadedBytes()).isEqualTo(downloadedBytes);
}
} }

View File

@ -16,6 +16,7 @@
package com.google.android.exoplayer2.source.hls.offline; package com.google.android.exoplayer2.source.hls.offline;
import android.net.Uri; import android.net.Uri;
import android.util.Pair;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
import com.google.android.exoplayer2.offline.SegmentDownloader; import com.google.android.exoplayer2.offline.SegmentDownloader;
@ -70,17 +71,17 @@ public final class HlsDownloader extends SegmentDownloader<HlsMasterPlaylist, Re
} }
@Override @Override
protected List<Segment> getSegments( protected Pair<List<Segment>, Boolean> getSegments(
DataSource dataSource, DataSource dataSource, HlsMasterPlaylist manifest, boolean allowIndexLoadErrors)
HlsMasterPlaylist manifest, throws IOException {
boolean allowIndexLoadErrors)
throws InterruptedException, IOException {
HashSet<Uri> encryptionKeyUris = new HashSet<>(); HashSet<Uri> encryptionKeyUris = new HashSet<>();
ArrayList<HlsUrl> renditionUrls = new ArrayList<>(); ArrayList<HlsUrl> renditionUrls = new ArrayList<>();
renditionUrls.addAll(manifest.variants); renditionUrls.addAll(manifest.variants);
renditionUrls.addAll(manifest.audios); renditionUrls.addAll(manifest.audios);
renditionUrls.addAll(manifest.subtitles); renditionUrls.addAll(manifest.subtitles);
ArrayList<Segment> segments = new ArrayList<>(); ArrayList<Segment> segments = new ArrayList<>();
boolean segmentListComplete = true;
for (HlsUrl renditionUrl : renditionUrls) { for (HlsUrl renditionUrl : renditionUrls) {
HlsMediaPlaylist mediaPlaylist = null; HlsMediaPlaylist mediaPlaylist = null;
Uri uri = UriUtil.resolveToUri(manifest.baseUri, renditionUrl.url); Uri uri = UriUtil.resolveToUri(manifest.baseUri, renditionUrl.url);
@ -90,6 +91,7 @@ public final class HlsDownloader extends SegmentDownloader<HlsMasterPlaylist, Re
if (!allowIndexLoadErrors) { if (!allowIndexLoadErrors) {
throw e; throw e;
} }
segmentListComplete = false;
} }
segments.add(new Segment(mediaPlaylist != null ? mediaPlaylist.startTimeUs : Long.MIN_VALUE, segments.add(new Segment(mediaPlaylist != null ? mediaPlaylist.startTimeUs : Long.MIN_VALUE,
new DataSpec(uri))); new DataSpec(uri)));
@ -109,7 +111,7 @@ public final class HlsDownloader extends SegmentDownloader<HlsMasterPlaylist, Re
addSegment(segments, mediaPlaylist, segment, encryptionKeyUris); addSegment(segments, mediaPlaylist, segment, encryptionKeyUris);
} }
} }
return segments; return Pair.<List<Segment>, Boolean>create(segments, segmentListComplete);
} }
private static HlsPlaylist loadManifest(DataSource dataSource, Uri uri) throws IOException { private static HlsPlaylist loadManifest(DataSource dataSource, Uri uri) throws IOException {

View File

@ -111,8 +111,6 @@ public class HlsDownloaderTest {
hlsDownloader.selectRepresentations(getKeys(MEDIA_PLAYLIST_1_URI)); hlsDownloader.selectRepresentations(getKeys(MEDIA_PLAYLIST_1_URI));
hlsDownloader.download(); hlsDownloader.download();
assertThat(hlsDownloader.getTotalSegments()).isEqualTo(4);
assertThat(hlsDownloader.getDownloadedSegments()).isEqualTo(4);
assertThat(hlsDownloader.getDownloadedBytes()) assertThat(hlsDownloader.getDownloadedBytes())
.isEqualTo(MEDIA_PLAYLIST_DATA.length + 10 + 11 + 12); .isEqualTo(MEDIA_PLAYLIST_DATA.length + 10 + 11 + 12);
} }
@ -126,8 +124,6 @@ public class HlsDownloaderTest {
newHlsDownloader.selectRepresentations(getKeys(MEDIA_PLAYLIST_1_URI)); newHlsDownloader.selectRepresentations(getKeys(MEDIA_PLAYLIST_1_URI));
newHlsDownloader.init(); newHlsDownloader.init();
assertThat(newHlsDownloader.getTotalSegments()).isEqualTo(4);
assertThat(newHlsDownloader.getDownloadedSegments()).isEqualTo(4);
assertThat(newHlsDownloader.getDownloadedBytes()) assertThat(newHlsDownloader.getDownloadedBytes())
.isEqualTo(MEDIA_PLAYLIST_DATA.length + 10 + 11 + 12); .isEqualTo(MEDIA_PLAYLIST_DATA.length + 10 + 11 + 12);
} }

View File

@ -16,6 +16,7 @@
package com.google.android.exoplayer2.source.smoothstreaming.offline; package com.google.android.exoplayer2.source.smoothstreaming.offline;
import android.net.Uri; import android.net.Uri;
import android.util.Pair;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
import com.google.android.exoplayer2.offline.SegmentDownloader; import com.google.android.exoplayer2.offline.SegmentDownloader;
@ -34,9 +35,6 @@ import java.util.List;
/** /**
* Helper class to download SmoothStreaming streams. * Helper class to download SmoothStreaming streams.
* *
* <p>Except {@link #getTotalSegments()}, {@link #getDownloadedSegments()} and {@link
* #getDownloadedBytes()}, this class isn't thread safe.
*
* <p>Example usage: * <p>Example usage:
* *
* <pre>{@code * <pre>{@code
@ -84,9 +82,8 @@ public final class SsDownloader extends SegmentDownloader<SsManifest, TrackKey>
} }
@Override @Override
protected List<Segment> getSegments( protected Pair<List<Segment>, Boolean> getSegments(
DataSource dataSource, SsManifest manifest, boolean allowIndexLoadErrors) DataSource dataSource, SsManifest manifest, boolean allowIndexLoadErrors) throws IOException {
throws InterruptedException, IOException {
ArrayList<Segment> segments = new ArrayList<>(); ArrayList<Segment> segments = new ArrayList<>();
for (StreamElement streamElement : manifest.streamElements) { for (StreamElement streamElement : manifest.streamElements) {
for (int i = 0; i < streamElement.formats.length; i++) { for (int i = 0; i < streamElement.formats.length; i++) {
@ -98,7 +95,7 @@ public final class SsDownloader extends SegmentDownloader<SsManifest, TrackKey>
} }
} }
} }
return segments; return Pair.<List<Segment>, Boolean>create(segments, true);
} }
} }

View File

@ -19,6 +19,7 @@ import android.app.Notification;
import android.content.Context; import android.content.Context;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationCompat;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloadManager;
import com.google.android.exoplayer2.offline.DownloadManager.DownloadState; import com.google.android.exoplayer2.offline.DownloadManager.DownloadState;
import com.google.android.exoplayer2.util.ErrorMessageProvider; import com.google.android.exoplayer2.util.ErrorMessageProvider;
@ -48,31 +49,31 @@ public final class DownloadNotificationUtil {
String channelId, String channelId,
@Nullable String message) { @Nullable String message) {
float totalPercentage = 0; float totalPercentage = 0;
int determinatePercentageCount = 0; int downloadTaskCount = 0;
boolean isAnyDownloadActive = false; boolean allDownloadPercentagesUnknown = true;
boolean haveDownloadedBytes = false;
for (DownloadState downloadState : downloadStates) { for (DownloadState downloadState : downloadStates) {
if (downloadState.downloadAction.isRemoveAction if (downloadState.downloadAction.isRemoveAction
|| downloadState.state != DownloadState.STATE_STARTED) { || downloadState.state != DownloadState.STATE_STARTED) {
continue; continue;
} }
float percentage = downloadState.downloadPercentage; if (downloadState.downloadPercentage != C.PERCENTAGE_UNSET) {
if (!Float.isNaN(percentage)) { allDownloadPercentagesUnknown = false;
totalPercentage += percentage; totalPercentage += downloadState.downloadPercentage;
determinatePercentageCount++;
} }
isAnyDownloadActive = true; haveDownloadedBytes |= downloadState.downloadedBytes > 0;
downloadTaskCount++;
} }
int titleStringId = isAnyDownloadActive ? R.string.exo_download_downloading : NULL_STRING_ID; boolean haveDownloadTasks = downloadTaskCount > 0;
int titleStringId = haveDownloadTasks ? R.string.exo_download_downloading : NULL_STRING_ID;
NotificationCompat.Builder notificationBuilder = NotificationCompat.Builder notificationBuilder =
createNotificationBuilder(context, smallIcon, channelId, message, titleStringId); createNotificationBuilder(context, smallIcon, channelId, message, titleStringId);
int progress = haveDownloadTasks ? (int) (totalPercentage / downloadTaskCount) : 0;
boolean indeterminate = allDownloadPercentagesUnknown && haveDownloadedBytes;
notificationBuilder.setProgress(/* max= */ 100, progress, indeterminate);
notificationBuilder.setOngoing(true); notificationBuilder.setOngoing(true);
int max = 100;
int progress = (int) (totalPercentage / determinatePercentageCount);
boolean indeterminate = determinatePercentageCount == 0;
notificationBuilder.setProgress(max, progress, indeterminate);
notificationBuilder.setShowWhen(false); notificationBuilder.setShowWhen(false);
return notificationBuilder.build(); return notificationBuilder.build();
} }