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) {
this.key = key;
this.title = title;
this.percentDownloaded = (int) percentDownloaded;
this.percentDownloaded =
(int) (percentDownloaded == C.PERCENTAGE_UNSET ? 0 : percentDownloaded);
}
@Override

View File

@ -64,6 +64,9 @@ public final class C {
*/
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.
*/

View File

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

View File

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

View File

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

View File

@ -17,6 +17,7 @@ package com.google.android.exoplayer2.offline;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.util.Pair;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec;
@ -33,9 +34,6 @@ import java.util.List;
/**
* 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 <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);
}
/** Returns keys for all representations. */
public abstract K[] getAllRepresentationKeys() throws IOException;
/**
* 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
@ -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[])
*/
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 IOException Thrown when there is an error downloading.
* @throws InterruptedException If the thread has been interrupted.
* @see #getTotalSegments()
* @see #getDownloadedSegments()
* @see #getDownloadedBytes()
*/
@Override
public final void init() throws InterruptedException, IOException {
public final void init() throws IOException, InterruptedException {
try {
getManifestIfNeeded(true);
} catch (IOException e) {
@ -140,7 +129,7 @@ public abstract class SegmentDownloader<M extends FilterableManifest<M, K>, K>
}
try {
initStatus(true);
} catch (IOException | InterruptedException e) {
} catch (IOException e) {
resetCounters();
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
* download.
*
* @throws IOException Thrown when there is an io error while downloading.
* @throws DownloadException Thrown if the media cannot be downloaded.
* @throws IOException Thrown when there is an error downloading.
* @throws InterruptedException If the thread has been interrupted.
*/
@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
public final long getDownloadedBytes() {
return downloadedBytes;
@ -211,7 +173,7 @@ public abstract class SegmentDownloader<M extends FilterableManifest<M, K>, K>
int totalSegments = this.totalSegments;
int downloadedSegments = this.downloadedSegments;
if (totalSegments == C.LENGTH_UNSET || downloadedSegments == C.LENGTH_UNSET) {
return Float.NaN;
return C.PERCENTAGE_UNSET;
}
return totalSegments == 0 ? 100f : (downloadedSegments * 100f) / totalSegments;
}
@ -228,7 +190,7 @@ public abstract class SegmentDownloader<M extends FilterableManifest<M, K>, K>
if (manifest != null) {
List<Segment> segments = null;
try {
segments = getSegments(offlineDataSource, manifest, true);
segments = getSegments(offlineDataSource, manifest, true).first;
} catch (IOException e) {
// 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 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.
* @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)
throws InterruptedException, IOException;
private void resetCounters() {
totalSegments = C.LENGTH_UNSET;
downloadedSegments = C.LENGTH_UNSET;
downloadedBytes = C.LENGTH_UNSET;
downloadedSegments = 0;
downloadedBytes = 0;
}
private void remove(Uri uri) {
@ -289,9 +252,11 @@ public abstract class SegmentDownloader<M extends FilterableManifest<M, K>, K>
throws IOException, InterruptedException {
DataSource dataSource = getDataSource(offline);
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();
totalSegments = segments.size();
totalSegments = isSegmentListComplete ? segments.size() : C.LENGTH_UNSET;
downloadedSegments = 0;
downloadedBytes = 0;
for (int i = segments.size() - 1; i >= 0; i--) {

View File

@ -16,6 +16,7 @@
package com.google.android.exoplayer2.source.dash.offline;
import android.net.Uri;
import android.util.Pair;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.ChunkIndex;
import com.google.android.exoplayer2.offline.DownloadException;
@ -39,9 +40,6 @@ import java.util.List;
/**
* Helper class to download DASH streams.
*
* <p>Except {@link #getTotalSegments()}, {@link #getDownloadedSegments()} and {@link
* #getDownloadedBytes()}, this class isn't thread safe.
*
* <p>Example usage:
*
* <pre>{@code
@ -89,29 +87,32 @@ public final class DashDownloader extends SegmentDownloader<DashManifest, Repres
}
@Override
protected List<Segment> getSegments(
protected Pair<List<Segment>, Boolean> getSegments(
DataSource dataSource, DashManifest manifest, boolean allowIndexLoadErrors)
throws InterruptedException, IOException {
ArrayList<Segment> segments = new ArrayList<>();
boolean segmentListComplete = true;
for (int i = 0; i < manifest.getPeriodCount(); i++) {
Period period = manifest.getPeriod(i);
long periodStartUs = C.msToUs(period.startMs);
long periodDurationUs = manifest.getPeriodDurationUs(i);
List<AdaptationSet> adaptationSets = period.adaptationSets;
for (int j = 0; j < adaptationSets.size(); j++) {
addSegmentsForAdaptationSet(
if (!addSegmentsForAdaptationSet(
dataSource,
adaptationSets.get(j),
periodStartUs,
periodDurationUs,
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,
AdaptationSet adaptationSet,
long periodStartUs,
@ -119,6 +120,7 @@ public final class DashDownloader extends SegmentDownloader<DashManifest, Repres
boolean allowIndexLoadErrors,
ArrayList<Segment> out)
throws IOException, InterruptedException {
boolean segmentListComplete = true;
for (int i = 0; i < adaptationSet.representations.size(); i++) {
Representation representation = adaptationSet.representations.get(i);
DashSegmentIndex index;
@ -129,12 +131,12 @@ public final class DashDownloader extends SegmentDownloader<DashManifest, Repres
throw new DownloadException("Missing segment index");
}
} catch (IOException e) {
if (allowIndexLoadErrors) {
// Loading failed, but load errors are allowed. Advance to the next representation.
continue;
} else {
if (!allowIndexLoadErrors) {
throw e;
}
// Loading failed, but load errors are allowed. Advance to the next representation.
segmentListComplete = false;
continue;
}
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);
}
}
return segmentListComplete;
}
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.when;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.offline.DownloadException;
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
import com.google.android.exoplayer2.source.dash.manifest.DashManifest;
@ -65,7 +64,7 @@ public class DashDownloaderTest {
}
@After
public void tearDown() throws Exception {
public void tearDown() {
Util.recursiveDelete(tempFolder);
}
@ -311,11 +310,11 @@ public class DashDownloaderTest {
.setRandomData("audio_segment_3", 6);
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.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
try {
@ -325,11 +324,10 @@ public class DashDownloaderTest {
// ignore
}
dashDownloader.init();
assertCounters(dashDownloader, 4, 2, 10 + 4 + 2);
assertThat(dashDownloader.getDownloadedBytes()).isEqualTo(10 + 4 + 2);
dashDownloader.download();
assertCounters(dashDownloader, 4, 4, 10 + 4 + 5 + 6);
assertThat(dashDownloader.getDownloadedBytes()).isEqualTo(10 + 4 + 5 + 6);
}
@Test
@ -399,13 +397,4 @@ public class DashDownloaderTest {
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;
import android.net.Uri;
import android.util.Pair;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
import com.google.android.exoplayer2.offline.SegmentDownloader;
@ -70,17 +71,17 @@ public final class HlsDownloader extends SegmentDownloader<HlsMasterPlaylist, Re
}
@Override
protected List<Segment> getSegments(
DataSource dataSource,
HlsMasterPlaylist manifest,
boolean allowIndexLoadErrors)
throws InterruptedException, IOException {
protected Pair<List<Segment>, Boolean> getSegments(
DataSource dataSource, HlsMasterPlaylist manifest, boolean allowIndexLoadErrors)
throws IOException {
HashSet<Uri> encryptionKeyUris = new HashSet<>();
ArrayList<HlsUrl> renditionUrls = new ArrayList<>();
renditionUrls.addAll(manifest.variants);
renditionUrls.addAll(manifest.audios);
renditionUrls.addAll(manifest.subtitles);
ArrayList<Segment> segments = new ArrayList<>();
boolean segmentListComplete = true;
for (HlsUrl renditionUrl : renditionUrls) {
HlsMediaPlaylist mediaPlaylist = null;
Uri uri = UriUtil.resolveToUri(manifest.baseUri, renditionUrl.url);
@ -90,6 +91,7 @@ public final class HlsDownloader extends SegmentDownloader<HlsMasterPlaylist, Re
if (!allowIndexLoadErrors) {
throw e;
}
segmentListComplete = false;
}
segments.add(new Segment(mediaPlaylist != null ? mediaPlaylist.startTimeUs : Long.MIN_VALUE,
new DataSpec(uri)));
@ -109,7 +111,7 @@ public final class HlsDownloader extends SegmentDownloader<HlsMasterPlaylist, Re
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 {

View File

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

View File

@ -16,6 +16,7 @@
package com.google.android.exoplayer2.source.smoothstreaming.offline;
import android.net.Uri;
import android.util.Pair;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
import com.google.android.exoplayer2.offline.SegmentDownloader;
@ -34,9 +35,6 @@ import java.util.List;
/**
* Helper class to download SmoothStreaming streams.
*
* <p>Except {@link #getTotalSegments()}, {@link #getDownloadedSegments()} and {@link
* #getDownloadedBytes()}, this class isn't thread safe.
*
* <p>Example usage:
*
* <pre>{@code
@ -84,9 +82,8 @@ public final class SsDownloader extends SegmentDownloader<SsManifest, TrackKey>
}
@Override
protected List<Segment> getSegments(
DataSource dataSource, SsManifest manifest, boolean allowIndexLoadErrors)
throws InterruptedException, IOException {
protected Pair<List<Segment>, Boolean> getSegments(
DataSource dataSource, SsManifest manifest, boolean allowIndexLoadErrors) throws IOException {
ArrayList<Segment> segments = new ArrayList<>();
for (StreamElement streamElement : manifest.streamElements) {
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.support.annotation.Nullable;
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.DownloadState;
import com.google.android.exoplayer2.util.ErrorMessageProvider;
@ -48,31 +49,31 @@ public final class DownloadNotificationUtil {
String channelId,
@Nullable String message) {
float totalPercentage = 0;
int determinatePercentageCount = 0;
boolean isAnyDownloadActive = false;
int downloadTaskCount = 0;
boolean allDownloadPercentagesUnknown = true;
boolean haveDownloadedBytes = false;
for (DownloadState downloadState : downloadStates) {
if (downloadState.downloadAction.isRemoveAction
|| downloadState.state != DownloadState.STATE_STARTED) {
continue;
}
float percentage = downloadState.downloadPercentage;
if (!Float.isNaN(percentage)) {
totalPercentage += percentage;
determinatePercentageCount++;
if (downloadState.downloadPercentage != C.PERCENTAGE_UNSET) {
allDownloadPercentagesUnknown = false;
totalPercentage += downloadState.downloadPercentage;
}
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 =
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);
int max = 100;
int progress = (int) (totalPercentage / determinatePercentageCount);
boolean indeterminate = determinatePercentageCount == 0;
notificationBuilder.setProgress(max, progress, indeterminate);
notificationBuilder.setShowWhen(false);
return notificationBuilder.build();
}