From e4369b2317d0fcaac60064551c3bd7cfd16c8b48 Mon Sep 17 00:00:00 2001 From: tianyifeng Date: Wed, 26 Feb 2025 07:44:19 -0800 Subject: [PATCH] Allow setting time range for adaptive media in DownloadRequest PiperOrigin-RevId: 731314103 --- .../exoplayer/offline/DownloadRequest.java | 105 +++++++++++- .../offline/DownloadRequestTest.java | 161 +++++++++++++----- 2 files changed, 222 insertions(+), 44 deletions(-) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DownloadRequest.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DownloadRequest.java index 3395ba44e0..1befb5a535 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DownloadRequest.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DownloadRequest.java @@ -54,12 +54,14 @@ public final class DownloadRequest implements Parcelable { @Nullable private String customCacheKey; @Nullable private byte[] data; @Nullable private ByteRange byteRange; + @Nullable private TimeRange timeRange; /** Creates a new instance with the specified id and uri. */ public Builder(String id, Uri uri) { this.id = id; this.uri = uri; this.byteRange = null; + this.timeRange = null; } /** Sets the {@link DownloadRequest#mimeType}. */ @@ -112,6 +114,22 @@ public final class DownloadRequest implements Parcelable { return this; } + /** + * Sets the time range to be downloaded. + * + *

This will be ignored progressive downloads. + * + * @param startPositionUs The start position in microseconds that the download should start + * from. + * @param durationUs The duration in microseconds from the {@code startPositionUs} to be + * downloaded, or @link C#TIME_UNSET} if the media should be downloaded to the end. + */ + @CanIgnoreReturnValue + public Builder setTimeRange(long startPositionUs, long durationUs) { + this.timeRange = new TimeRange(startPositionUs, durationUs); + return this; + } + public DownloadRequest build() { return new DownloadRequest( id, @@ -121,7 +139,8 @@ public final class DownloadRequest implements Parcelable { keySetId, customCacheKey, data, - byteRange); + byteRange, + timeRange); } } @@ -156,6 +175,9 @@ public final class DownloadRequest implements Parcelable { /** The byte range to be downloaded. Must be null for DASH, HLS and SmoothStreaming downloads. */ @Nullable public final ByteRange byteRange; + /** The time range to be downloaded. Must be null progressive downloads. */ + @Nullable public final TimeRange timeRange; + /** * @param id See {@link #id}. * @param uri See {@link #uri}. @@ -163,6 +185,8 @@ public final class DownloadRequest implements Parcelable { * @param streamKeys See {@link #streamKeys}. * @param customCacheKey See {@link #customCacheKey}. * @param data See {@link #data}. + * @param byteRange See {@link #byteRange}. + * @param timeRange See {@link #timeRange}. */ private DownloadRequest( String id, @@ -172,15 +196,18 @@ public final class DownloadRequest implements Parcelable { @Nullable byte[] keySetId, @Nullable String customCacheKey, @Nullable byte[] data, - @Nullable ByteRange byteRange) { + @Nullable ByteRange byteRange, + @Nullable TimeRange timeRange) { @C.ContentType int contentType = Util.inferContentTypeForUriAndMimeType(uri, mimeType); if (contentType == C.CONTENT_TYPE_DASH || contentType == C.CONTENT_TYPE_HLS || contentType == C.CONTENT_TYPE_SS) { checkArgument(customCacheKey == null, "customCacheKey must be null for type: " + contentType); this.byteRange = null; + this.timeRange = timeRange; } else { this.byteRange = byteRange; + this.timeRange = null; } this.id = id; this.uri = uri; @@ -207,6 +234,7 @@ public final class DownloadRequest implements Parcelable { customCacheKey = in.readString(); data = castNonNull(in.createByteArray()); byteRange = in.readParcelable(ByteRange.class.getClassLoader()); + timeRange = in.readParcelable(TimeRange.class.getClassLoader()); } /** @@ -217,7 +245,7 @@ public final class DownloadRequest implements Parcelable { */ public DownloadRequest copyWithId(String id) { return new DownloadRequest( - id, uri, mimeType, streamKeys, keySetId, customCacheKey, data, byteRange); + id, uri, mimeType, streamKeys, keySetId, customCacheKey, data, byteRange, timeRange); } /** @@ -228,7 +256,7 @@ public final class DownloadRequest implements Parcelable { */ public DownloadRequest copyWithKeySetId(@Nullable byte[] keySetId) { return new DownloadRequest( - id, uri, mimeType, streamKeys, keySetId, customCacheKey, data, byteRange); + id, uri, mimeType, streamKeys, keySetId, customCacheKey, data, byteRange, timeRange); } /** @@ -265,7 +293,8 @@ public final class DownloadRequest implements Parcelable { newRequest.keySetId, newRequest.customCacheKey, newRequest.data, - newRequest.byteRange); + newRequest.byteRange, + newRequest.timeRange); } /** Returns a {@link MediaItem} for the content defined by the request. */ @@ -297,7 +326,8 @@ public final class DownloadRequest implements Parcelable { && Arrays.equals(keySetId, that.keySetId) && Objects.equals(customCacheKey, that.customCacheKey) && Arrays.equals(data, that.data) - && Objects.equals(byteRange, that.byteRange); + && Objects.equals(byteRange, that.byteRange) + && Objects.equals(timeRange, that.timeRange); } @Override @@ -310,6 +340,7 @@ public final class DownloadRequest implements Parcelable { result = 31 * result + (customCacheKey != null ? customCacheKey.hashCode() : 0); result = 31 * result + Arrays.hashCode(data); result = 31 * result + (byteRange != null ? byteRange.hashCode() : 0); + result = 31 * result + (timeRange != null ? timeRange.hashCode() : 0); return result; } @@ -333,6 +364,7 @@ public final class DownloadRequest implements Parcelable { dest.writeString(customCacheKey); dest.writeByteArray(data); dest.writeParcelable(byteRange, /* parcelableFlags= */ 0); + dest.writeParcelable(timeRange, /* parcelableFlags= */ 0); } public static final Parcelable.Creator CREATOR = @@ -410,4 +442,65 @@ public final class DownloadRequest implements Parcelable { } }; } + + /** Defines the time range. */ + public static final class TimeRange implements Parcelable { + + /** The start position of the time range, in microseconds. */ + public final long startPositionUs; + + /** The duration of the time range, in microseconds. */ + public final long durationUs; + + /* package */ TimeRange(long startPositionUs, long durationUs) { + checkArgument(durationUs >= 0 || durationUs == C.TIME_UNSET); + this.startPositionUs = startPositionUs; + this.durationUs = durationUs; + } + + /* package */ TimeRange(Parcel in) { + this(in.readLong(), in.readLong()); + } + + @Override + public boolean equals(@Nullable Object o) { + if (!(o instanceof TimeRange)) { + return false; + } + TimeRange that = (TimeRange) o; + return startPositionUs == that.startPositionUs && durationUs == that.durationUs; + } + + @Override + public int hashCode() { + int result = 31 * (int) startPositionUs; + result = 31 * result + (int) durationUs; + return result; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeLong(startPositionUs); + dest.writeLong(durationUs); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public TimeRange createFromParcel(Parcel in) { + return new TimeRange(in); + } + + @Override + public TimeRange[] newArray(int size) { + return new TimeRange[size]; + } + }; + } } diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/offline/DownloadRequestTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/offline/DownloadRequestTest.java index 0c5e0e34ac..e4dccb6b23 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/offline/DownloadRequestTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/offline/DownloadRequestTest.java @@ -32,20 +32,45 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class DownloadRequestTest { - private Uri uri1; - private Uri uri2; + private Uri progressiveUri1; + private Uri progressiveUri2; + private Uri adaptiveUri1; + private Uri adaptiveUri2; @Before public void setUp() { - uri1 = Uri.parse("http://test/1.uri"); - uri2 = Uri.parse("http://test/2.uri"); + progressiveUri1 = Uri.parse("http://test/1.uri"); + progressiveUri2 = Uri.parse("http://test/2.uri"); + adaptiveUri1 = Uri.parse("http://test/1.m3u8"); + adaptiveUri2 = Uri.parse("http://test/2.m3u8"); + } + + @Test + public void createRequestForProgressiveStream_ignoreTimeRangeField() { + DownloadRequest downloadRequest = + new DownloadRequest.Builder(/* id= */ "id1", progressiveUri1) + .setTimeRange(/* startPositionUs= */ 0, /* durationUs= */ 10_000) + .build(); + + assertThat(downloadRequest.timeRange).isNull(); + } + + @Test + public void createRequestForAdaptiveStream_ignoreByteRangeField() { + DownloadRequest downloadRequest = + new DownloadRequest.Builder(/* id= */ "id1", adaptiveUri1) + .setByteRange(/* offset= */ 0, /* length= */ 10) + .build(); + + assertThat(downloadRequest.byteRange).isNull(); } @Test public void mergeRequests_withDifferentIds_fails() { - - DownloadRequest request1 = new DownloadRequest.Builder(/* id= */ "id1", uri1).build(); - DownloadRequest request2 = new DownloadRequest.Builder(/* id= */ "id2", uri2).build(); + DownloadRequest request1 = + new DownloadRequest.Builder(/* id= */ "id1", progressiveUri1).build(); + DownloadRequest request2 = + new DownloadRequest.Builder(/* id= */ "id2", progressiveUri2).build(); try { request1.copyWithMergedRequest(request2); @@ -57,7 +82,7 @@ public class DownloadRequestTest { @Test public void mergeRequest_withSameRequest() { - DownloadRequest request1 = createRequest(uri1, new StreamKey(0, 0, 0)); + DownloadRequest request1 = createRequest(progressiveUri1, new StreamKey(0, 0, 0)); DownloadRequest mergedRequest = request1.copyWithMergedRequest(request1); assertEqual(request1, mergedRequest); @@ -65,8 +90,8 @@ public class DownloadRequestTest { @Test public void mergeRequests_withEmptyStreamKeys() { - DownloadRequest request1 = createRequest(uri1, new StreamKey(0, 0, 0)); - DownloadRequest request2 = createRequest(uri1); + DownloadRequest request1 = createRequest(progressiveUri1, new StreamKey(0, 0, 0)); + DownloadRequest request2 = createRequest(progressiveUri1); // If either of the requests have empty streamKeys, the merge should have empty streamKeys. DownloadRequest mergedRequest = request1.copyWithMergedRequest(request2); @@ -81,8 +106,8 @@ public class DownloadRequestTest { StreamKey streamKey1 = new StreamKey(0, 1, 2); StreamKey streamKey2 = new StreamKey(3, 4, 5); StreamKey streamKey3 = new StreamKey(6, 7, 8); - DownloadRequest request1 = createRequest(uri1, streamKey1, streamKey2); - DownloadRequest request2 = createRequest(uri1, streamKey2, streamKey3); + DownloadRequest request1 = createRequest(progressiveUri1, streamKey1, streamKey2); + DownloadRequest request2 = createRequest(progressiveUri1, streamKey2, streamKey3); // Merged streamKeys should be in their original order without duplicates. DownloadRequest mergedRequest = request1.copyWithMergedRequest(request2); @@ -100,23 +125,23 @@ public class DownloadRequestTest { byte[] data2 = new byte[] {9, 10, 11}; DownloadRequest request1 = - new DownloadRequest.Builder(/* id= */ "id1", uri1) + new DownloadRequest.Builder(/* id= */ "id1", progressiveUri1) .setKeySetId(keySetId1) .setCustomCacheKey("key1") .setData(data1) .setByteRange(/* offset= */ 0, /* length= */ 10) .build(); DownloadRequest request2 = - new DownloadRequest.Builder(/* id= */ "id1", uri2) + new DownloadRequest.Builder(/* id= */ "id1", progressiveUri2) .setKeySetId(keySetId2) .setCustomCacheKey("key2") .setData(data2) .setByteRange(/* offset= */ 10, /* length= */ 20) .build(); - // uri, keySetId, customCacheKey and data should be from the request being merged. + // uri, keySetId, customCacheKey, data and byteRange should be from the request being merged. DownloadRequest mergedRequest = request1.copyWithMergedRequest(request2); - assertThat(mergedRequest.uri).isEqualTo(uri2); + assertThat(mergedRequest.uri).isEqualTo(progressiveUri2); assertThat(mergedRequest.keySetId).isEqualTo(keySetId2); assertThat(mergedRequest.customCacheKey).isEqualTo("key2"); assertThat(mergedRequest.data).isEqualTo(data2); @@ -124,12 +149,40 @@ public class DownloadRequestTest { assertThat(mergedRequest.byteRange.length).isEqualTo(20); mergedRequest = request2.copyWithMergedRequest(request1); - assertThat(mergedRequest.uri).isEqualTo(uri1); + assertThat(mergedRequest.uri).isEqualTo(progressiveUri1); assertThat(mergedRequest.keySetId).isEqualTo(keySetId1); assertThat(mergedRequest.customCacheKey).isEqualTo("key1"); assertThat(mergedRequest.data).isEqualTo(data1); assertThat(mergedRequest.byteRange.offset).isEqualTo(0); assertThat(mergedRequest.byteRange.length).isEqualTo(10); + + DownloadRequest adaptiveRequest1 = + new DownloadRequest.Builder(/* id= */ "id1", adaptiveUri1) + .setKeySetId(keySetId1) + .setData(data1) + .setTimeRange(/* startPositionUs= */ 0, /* durationUs= */ 10_000) + .build(); + DownloadRequest adaptiveRequest2 = + new DownloadRequest.Builder(/* id= */ "id1", adaptiveUri2) + .setKeySetId(keySetId2) + .setData(data2) + .setTimeRange(/* startPositionUs= */ 10_000, /* durationUs= */ 20_000) + .build(); + + // uri, keySetId, data and timeRange should be from the request being merged. + mergedRequest = adaptiveRequest1.copyWithMergedRequest(adaptiveRequest2); + assertThat(mergedRequest.uri).isEqualTo(adaptiveUri2); + assertThat(mergedRequest.keySetId).isEqualTo(keySetId2); + assertThat(mergedRequest.data).isEqualTo(data2); + assertThat(mergedRequest.timeRange.startPositionUs).isEqualTo(10_000); + assertThat(mergedRequest.timeRange.durationUs).isEqualTo(20_000); + + mergedRequest = adaptiveRequest2.copyWithMergedRequest(adaptiveRequest1); + assertThat(mergedRequest.uri).isEqualTo(adaptiveUri1); + assertThat(mergedRequest.keySetId).isEqualTo(keySetId1); + assertThat(mergedRequest.data).isEqualTo(data1); + assertThat(mergedRequest.timeRange.startPositionUs).isEqualTo(0); + assertThat(mergedRequest.timeRange.durationUs).isEqualTo(10_000); } @Test @@ -158,53 +211,78 @@ public class DownloadRequestTest { @SuppressWarnings("EqualsWithItself") @Test public void equals() { - DownloadRequest request1 = createRequest(uri1); + DownloadRequest request1 = createRequest(progressiveUri1); assertThat(request1.equals(request1)).isTrue(); - DownloadRequest request2 = createRequest(uri1); - DownloadRequest request3 = createRequest(uri1); + DownloadRequest request2 = createRequest(progressiveUri1); + DownloadRequest request3 = createRequest(progressiveUri1); assertEqual(request2, request3); - DownloadRequest request4 = createRequest(uri1); - DownloadRequest request5 = createRequest(uri1, new StreamKey(0, 0, 0)); + DownloadRequest request4 = createRequest(progressiveUri1); + DownloadRequest request5 = createRequest(progressiveUri1, new StreamKey(0, 0, 0)); assertNotEqual(request4, request5); - DownloadRequest request6 = createRequest(uri1, new StreamKey(0, 1, 1)); - DownloadRequest request7 = createRequest(uri1, new StreamKey(0, 0, 0)); + DownloadRequest request6 = createRequest(progressiveUri1, new StreamKey(0, 1, 1)); + DownloadRequest request7 = createRequest(progressiveUri1, new StreamKey(0, 0, 0)); assertNotEqual(request6, request7); - DownloadRequest request8 = createRequest(uri1); - DownloadRequest request9 = createRequest(uri2); + DownloadRequest request8 = createRequest(progressiveUri1); + DownloadRequest request9 = createRequest(progressiveUri2); assertNotEqual(request8, request9); - DownloadRequest request10 = createRequest(uri1, new StreamKey(0, 0, 0), new StreamKey(0, 1, 1)); - DownloadRequest request11 = createRequest(uri1, new StreamKey(0, 1, 1), new StreamKey(0, 0, 0)); + DownloadRequest request10 = + createRequest(progressiveUri1, new StreamKey(0, 0, 0), new StreamKey(0, 1, 1)); + DownloadRequest request11 = + createRequest(progressiveUri1, new StreamKey(0, 1, 1), new StreamKey(0, 0, 0)); assertEqual(request10, request11); - DownloadRequest request12 = createRequest(uri1, new StreamKey(0, 0, 0)); - DownloadRequest request13 = createRequest(uri1, new StreamKey(0, 1, 1), new StreamKey(0, 0, 0)); + DownloadRequest request12 = createRequest(progressiveUri1, new StreamKey(0, 0, 0)); + DownloadRequest request13 = + createRequest(progressiveUri1, new StreamKey(0, 1, 1), new StreamKey(0, 0, 0)); assertNotEqual(request12, request13); - DownloadRequest request14 = createRequest(uri1); - DownloadRequest request15 = createRequest(uri1); + DownloadRequest request14 = createRequest(progressiveUri1); + DownloadRequest request15 = createRequest(progressiveUri1); assertEqual(request14, request15); - DownloadRequest request16 = createRequest(uri1); + DownloadRequest request16 = createRequest(progressiveUri1); DownloadRequest request17 = - createRequest(uri1, /* byteRangeOffset= */ 0, /* byteRangeLength= */ 20); + createRequest(progressiveUri1, /* byteRangeOffset= */ 0, /* byteRangeLength= */ 20); assertNotEqual(request16, request17); DownloadRequest request18 = - createRequest(uri1, /* byteRangeOffset= */ 0, /* byteRangeLength= */ 20); + createRequest(progressiveUri1, /* byteRangeOffset= */ 0, /* byteRangeLength= */ 20); DownloadRequest request19 = - createRequest(uri1, /* byteRangeOffset= */ 0, /* byteRangeLength= */ 20); + createRequest(progressiveUri1, /* byteRangeOffset= */ 0, /* byteRangeLength= */ 20); assertEqual(request18, request19); DownloadRequest request20 = - createRequest(uri1, /* byteRangeOffset= */ 0, /* byteRangeLength= */ 10); + createRequest(progressiveUri1, /* byteRangeOffset= */ 0, /* byteRangeLength= */ 10); DownloadRequest request21 = - createRequest(uri1, /* byteRangeOffset= */ 0, /* byteRangeLength= */ 20); + createRequest(progressiveUri1, /* byteRangeOffset= */ 0, /* byteRangeLength= */ 20); assertNotEqual(request20, request21); + + DownloadRequest request22 = createRequest(adaptiveUri1); + DownloadRequest request23 = + createRequestWithTimeRange( + adaptiveUri1, /* startPositionUs= */ 0, /* durationUs= */ 20_000); + assertNotEqual(request22, request23); + + DownloadRequest request24 = + createRequestWithTimeRange( + adaptiveUri1, /* startPositionUs= */ 0, /* durationUs= */ 20_000); + DownloadRequest request25 = + createRequestWithTimeRange( + adaptiveUri1, /* startPositionUs= */ 0, /* durationUs= */ 20_000); + assertEqual(request24, request25); + + DownloadRequest request26 = + createRequestWithTimeRange( + adaptiveUri1, /* startPositionUs= */ 0, /* durationUs= */ 10_000); + DownloadRequest request27 = + createRequestWithTimeRange( + adaptiveUri1, /* startPositionUs= */ 0, /* durationUs= */ 20_000); + assertNotEqual(request26, request27); } private static void assertNotEqual(DownloadRequest request1, DownloadRequest request2) { @@ -228,4 +306,11 @@ public class DownloadRequestTest { .setByteRange(byteRangeOffset, byteRangeLength) .build(); } + + private static DownloadRequest createRequestWithTimeRange( + Uri uri, long startPositionUs, long durationUs) { + return new DownloadRequest.Builder(uri.toString(), uri) + .setTimeRange(startPositionUs, durationUs) + .build(); + } }