From c8e7ecd36794e4030ff1d0a56d25e5e0e6e0a216 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 14 Nov 2019 14:03:59 +0000 Subject: [PATCH] Merge consecutive segments for downloading. This speeds up downloads where segments have the same URL with different byte ranges. We limit the merged segments to 20 seconds to ensure the download progress of demuxed streams is roughly in line with the playable media duration. Issue:#5978 PiperOrigin-RevId: 280410761 --- RELEASENOTES.md | 2 + .../exoplayer2/offline/SegmentDownloader.java | 44 ++++++++++++++++++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index afdae31cf3..969b549083 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -137,6 +137,8 @@ [Cast demo app](https://github.com/google/ExoPlayer/tree/dev-v2/demos/cast). * TestUtils: Publish the `testutils` module to simplify unit testing with ExoPlayer ([#6267](https://github.com/google/ExoPlayer/issues/6267)). +* Downloads: Merge downloads in `SegmentDownloader` to improve overall download + speed ([#5978](https://github.com/google/ExoPlayer/issues/5978)). ### 2.10.7 (2019-11-12) ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java index 969003101f..5155685999 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java @@ -25,11 +25,13 @@ import com.google.android.exoplayer2.upstream.cache.Cache; import com.google.android.exoplayer2.upstream.cache.CacheDataSource; import com.google.android.exoplayer2.upstream.cache.CacheKeyFactory; import com.google.android.exoplayer2.upstream.cache.CacheUtil; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.PriorityTaskManager; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; @@ -62,6 +64,7 @@ public abstract class SegmentDownloader> impleme } private static final int BUFFER_SIZE_BYTES = 128 * 1024; + private static final long MAX_MERGED_SEGMENT_START_TIME_DIFF_US = 20 * C.MICROS_PER_SECOND; private final DataSpec manifestDataSpec; private final Cache cache; @@ -108,6 +111,8 @@ public abstract class SegmentDownloader> impleme manifest = manifest.copy(streamKeys); } List segments = getSegments(dataSource, manifest, /* allowIncompleteList= */ false); + Collections.sort(segments); + mergeSegments(segments, cacheKeyFactory); // Scan the segments, removing any that are fully downloaded. int totalSegments = segments.size(); @@ -134,7 +139,6 @@ public abstract class SegmentDownloader> impleme contentLength = C.LENGTH_UNSET; } } - Collections.sort(segments); // Download the segments. @Nullable ProgressNotifier progressNotifier = null; @@ -232,6 +236,44 @@ public abstract class SegmentDownloader> impleme /* flags= */ DataSpec.FLAG_ALLOW_GZIP); } + private static void mergeSegments(List segments, CacheKeyFactory keyFactory) { + HashMap lastIndexByCacheKey = new HashMap<>(); + int nextOutIndex = 0; + for (int i = 0; i < segments.size(); i++) { + Segment segment = segments.get(i); + String cacheKey = keyFactory.buildCacheKey(segment.dataSpec); + @Nullable Integer lastIndex = lastIndexByCacheKey.get(cacheKey); + @Nullable Segment lastSegment = lastIndex == null ? null : segments.get(lastIndex); + if (lastSegment == null + || segment.startTimeUs > lastSegment.startTimeUs + MAX_MERGED_SEGMENT_START_TIME_DIFF_US + || !canMergeSegments(lastSegment.dataSpec, segment.dataSpec)) { + lastIndexByCacheKey.put(cacheKey, nextOutIndex); + segments.set(nextOutIndex, segment); + nextOutIndex++; + } else { + long mergedLength = + segment.dataSpec.length == C.LENGTH_UNSET + ? C.LENGTH_UNSET + : lastSegment.dataSpec.length + segment.dataSpec.length; + DataSpec mergedDataSpec = lastSegment.dataSpec.subrange(/* offset= */ 0, mergedLength); + segments.set( + Assertions.checkNotNull(lastIndex), + new Segment(lastSegment.startTimeUs, mergedDataSpec)); + } + } + Util.removeRange(segments, /* fromIndex= */ nextOutIndex, /* toIndex= */ segments.size()); + } + + private static boolean canMergeSegments(DataSpec dataSpec1, DataSpec dataSpec2) { + return dataSpec1.uri.equals(dataSpec2.uri) + && dataSpec1.length != C.LENGTH_UNSET + && (dataSpec1.absoluteStreamPosition + dataSpec1.length == dataSpec2.absoluteStreamPosition) + && Util.areEqual(dataSpec1.key, dataSpec2.key) + && dataSpec1.flags == dataSpec2.flags + && dataSpec1.httpMethod == dataSpec2.httpMethod + && dataSpec1.httpRequestHeaders.equals(dataSpec2.httpRequestHeaders); + } + private static final class ProgressNotifier implements CacheUtil.ProgressListener { private final ProgressListener progressListener;