From e0d3cad8bb6563d3984d985f50e1896f2b6c7724 Mon Sep 17 00:00:00 2001 From: rohks Date: Sat, 19 Aug 2023 04:42:59 +0100 Subject: [PATCH] Add fields next object request (nor) and next range request (nrr) Added this CMCD-Request fields to Common Media Client Data (CMCD) logging. PiperOrigin-RevId: 558317146 --- RELEASENOTES.md | 3 + .../androidx/media3/common/util/UriUtil.java | 55 ++++++++++++ .../media3/common/util/UriUtilTest.java | 73 ++++++++++++++++ .../exoplayer/upstream/CmcdConfiguration.java | 22 ++++- .../upstream/CmcdHeadersFactory.java | 87 ++++++++++++++++++- .../dash/DefaultDashChunkSource.java | 53 ++++++++--- .../dash/DefaultDashChunkSourceTest.java | 8 +- .../media3/exoplayer/hls/HlsChunkSource.java | 55 ++++++++---- .../exoplayer/hls/HlsChunkSourceTest.java | 8 +- .../smoothstreaming/DefaultSsChunkSource.java | 36 ++++---- .../DefaultSsChunkSourceTest.java | 8 +- .../media/smooth-streaming/sample_ismc_1 | 1 + 12 files changed, 351 insertions(+), 58 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 78f5030669..e1ff64235c 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -4,6 +4,9 @@ * Common Library: * ExoPlayer: + * Add additional fields to Common Media Client Data (CMCD) logging: next + object request (`nor`) and next range request (`nrr`) + ([#8699](https://github.com/google/ExoPlayer/issues/8699)). * Transformer: * Track Selection: * Extractors: diff --git a/libraries/common/src/main/java/androidx/media3/common/util/UriUtil.java b/libraries/common/src/main/java/androidx/media3/common/util/UriUtil.java index cbe64a9a54..386e404e64 100644 --- a/libraries/common/src/main/java/androidx/media3/common/util/UriUtil.java +++ b/libraries/common/src/main/java/androidx/media3/common/util/UriUtil.java @@ -15,9 +15,13 @@ */ package androidx.media3.common.util; +import static java.lang.Math.min; + import android.net.Uri; import android.text.TextUtils; import androidx.annotation.Nullable; +import com.google.common.base.Ascii; +import java.util.List; /** Utility methods for manipulating URIs. */ @UnstableApi @@ -283,4 +287,55 @@ public final class UriUtil { indices[FRAGMENT] = fragmentIndex; return indices; } + + /** + * Calculates the relative path from a base URI to a target URI. + * + * @return The relative path from the base URI to the target URI, or {@code targetUri} if the URIs + * have different schemes or authorities. + */ + @UnstableApi + public static String getRelativePath(Uri baseUri, Uri targetUri) { + if (baseUri.isOpaque() || targetUri.isOpaque()) { + return targetUri.toString(); + } + + String baseUriScheme = baseUri.getScheme(); + String targetUriScheme = targetUri.getScheme(); + boolean isSameScheme = + baseUriScheme == null + ? targetUriScheme == null + : targetUriScheme != null && Ascii.equalsIgnoreCase(baseUriScheme, targetUriScheme); + if (!isSameScheme || !Util.areEqual(baseUri.getAuthority(), targetUri.getAuthority())) { + // Different schemes or authorities, cannot find relative path, return targetUri. + return targetUri.toString(); + } + + List basePathSegments = baseUri.getPathSegments(); + List targetPathSegments = targetUri.getPathSegments(); + + int commonPrefixCount = 0; + int minSize = min(basePathSegments.size(), targetPathSegments.size()); + + for (int i = 0; i < minSize; i++) { + if (!basePathSegments.get(i).equals(targetPathSegments.get(i))) { + break; + } + commonPrefixCount++; + } + + StringBuilder relativePath = new StringBuilder(); + for (int i = commonPrefixCount; i < basePathSegments.size(); i++) { + relativePath.append("../"); + } + + for (int i = commonPrefixCount; i < targetPathSegments.size(); i++) { + relativePath.append(targetPathSegments.get(i)); + if (i < targetPathSegments.size() - 1) { + relativePath.append("/"); + } + } + + return relativePath.toString(); + } } diff --git a/libraries/common/src/test/java/androidx/media3/common/util/UriUtilTest.java b/libraries/common/src/test/java/androidx/media3/common/util/UriUtilTest.java index 7b7f7595b3..92b71ca599 100644 --- a/libraries/common/src/test/java/androidx/media3/common/util/UriUtilTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/util/UriUtilTest.java @@ -155,4 +155,77 @@ public final class UriUtilTest { assertThat(UriUtil.isAbsolute("/path/to/file")).isFalse(); assertThat(UriUtil.isAbsolute("path/to/file")).isFalse(); } + + @Test + public void getRelativePath_withDifferentSchemes_shouldReturnTargetUri() { + Uri baseUri = Uri.parse("http://uri"); + Uri targetUri = Uri.parse("https://uri"); + + assertThat(UriUtil.getRelativePath(baseUri, targetUri)).isEqualTo(targetUri.toString()); + } + + @Test + public void getRelativePath_withDifferentAuthorities_shouldReturnTargetUri() { + Uri baseUri = Uri.parse("http://baseUri"); + Uri targetUri = Uri.parse("http://targetUri"); + + assertThat(UriUtil.getRelativePath(baseUri, targetUri)).isEqualTo(targetUri.toString()); + } + + @Test + public void getRelativePath_withoutSchemesAndDifferentAuthorities_shouldReturnTargetUri() { + Uri baseUri = Uri.parse("//baseUri/a"); + Uri targetUri = Uri.parse("//targetUri/b"); + + assertThat(UriUtil.getRelativePath(baseUri, targetUri)).isEqualTo(targetUri.toString()); + } + + @Test + public void getRelativePath_withoutSchemesAndSameAuthority_shouldReturnCorrectRelativePath() { + Uri baseUri = Uri.parse("//uri/a"); + Uri targetUri = Uri.parse("//uri/b"); + + assertThat(UriUtil.getRelativePath(baseUri, targetUri)).isEqualTo("../b"); + } + + @Test + public void + getRelativePath_withoutSchemesAndWithoutAuthorities_shouldReturnCorrectRelativePath() { + Uri baseUri = Uri.parse("a/b/c"); + Uri targetUri = Uri.parse("d/e/f"); + + assertThat(UriUtil.getRelativePath(baseUri, targetUri)).isEqualTo("../../../d/e/f"); + } + + @Test + public void getRelativePath_withEqualPathSegmentsLength_shouldReturnCorrectRelativePath() { + Uri baseUri = Uri.parse("http://uri/a/b/c"); + Uri targetUri = Uri.parse("http://uri/d/e/f"); + + assertThat(UriUtil.getRelativePath(baseUri, targetUri)).isEqualTo("../../../d/e/f"); + } + + @Test + public void getRelativePath_withUnEqualPathSegmentsLength_shouldReturnCorrectRelativePath() { + Uri baseUri = Uri.parse("http://uri/a/b/c"); + Uri targetUri = Uri.parse("http://uri/a/b/d/e/f"); + + assertThat(UriUtil.getRelativePath(baseUri, targetUri)).isEqualTo("../d/e/f"); + } + + @Test + public void getRelativePath_withEqualUris_shouldReturnEmptyString() { + Uri baseUri = Uri.parse("http://uri/a/b/c"); + Uri targetUri = Uri.parse("http://uri/a/b/c"); + + assertThat(UriUtil.getRelativePath(baseUri, targetUri)).isEmpty(); + } + + @Test + public void getRelativePath_nonHierarchicalUris_shouldReturnCorrectRelativePath() { + Uri baseUri = Uri.parse("schema:a@b"); + Uri targetUri = Uri.parse("schema:a@c"); + + assertThat(UriUtil.getRelativePath(baseUri, targetUri)).isEqualTo(targetUri.toString()); + } } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/CmcdConfiguration.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/CmcdConfiguration.java index 8d82138719..e73adcec96 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/CmcdConfiguration.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/CmcdConfiguration.java @@ -71,7 +71,9 @@ public final class CmcdConfiguration { KEY_BUFFER_STARVATION, KEY_DEADLINE, KEY_PLAYBACK_RATE, - KEY_STARTUP + KEY_STARTUP, + KEY_NEXT_OBJECT_REQUEST, + KEY_NEXT_RANGE_REQUEST }) @Documented @Target(TYPE_USE) @@ -100,6 +102,8 @@ public final class CmcdConfiguration { public static final String KEY_DEADLINE = "dl"; public static final String KEY_PLAYBACK_RATE = "pr"; public static final String KEY_STARTUP = "su"; + public static final String KEY_NEXT_OBJECT_REQUEST = "nor"; + public static final String KEY_NEXT_RANGE_REQUEST = "nrr"; /** * Factory for {@link CmcdConfiguration} instances. @@ -356,4 +360,20 @@ public final class CmcdConfiguration { public boolean isStartupLoggingAllowed() { return requestConfig.isKeyAllowed(KEY_STARTUP); } + + /** + * Returns whether logging next object request is allowed based on the {@linkplain RequestConfig + * request configuration}. + */ + public boolean isNextObjectRequestLoggingAllowed() { + return requestConfig.isKeyAllowed(KEY_NEXT_OBJECT_REQUEST); + } + + /** + * Returns whether logging next range request is allowed based on the {@linkplain RequestConfig + * request configuration}. + */ + public boolean isNextRangeRequestLoggingAllowed() { + return requestConfig.isKeyAllowed(KEY_NEXT_RANGE_REQUEST); + } } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/CmcdHeadersFactory.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/CmcdHeadersFactory.java index d95bfdd83c..f76807725e 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/CmcdHeadersFactory.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/CmcdHeadersFactory.java @@ -20,6 +20,7 @@ import static androidx.media3.common.util.Assertions.checkState; import static java.lang.Math.max; import static java.lang.annotation.ElementType.TYPE_USE; +import android.net.Uri; import android.text.TextUtils; import androidx.annotation.Nullable; import androidx.annotation.StringDef; @@ -149,6 +150,8 @@ public final class CmcdHeadersFactory { private final boolean isBufferEmpty; private long chunkDurationUs; private @Nullable @ObjectType String objectType; + @Nullable private String nextObjectRequest; + @Nullable private String nextRangeRequest; /** * Creates an instance. @@ -215,6 +218,30 @@ public final class CmcdHeadersFactory { return this; } + /** + * Sets the relative path of the next object to be requested. This can be used to trigger + * pre-fetching by the CDN. + * + *

Default is {@code null}. + */ + @CanIgnoreReturnValue + public CmcdHeadersFactory setNextObjectRequest(@Nullable String nextObjectRequest) { + this.nextObjectRequest = nextObjectRequest; + return this; + } + + /** + * Sets the byte range representing the partial object request. This can be used to trigger + * pre-fetching by the CDN. + * + *

Default is {@code null}. + */ + @CanIgnoreReturnValue + public CmcdHeadersFactory setNextRangeRequest(@Nullable String nextRangeRequest) { + this.nextRangeRequest = nextRangeRequest; + return this; + } + /** Creates and returns a new {@link ImmutableMap} containing the CMCD HTTP request headers. */ public ImmutableMap<@CmcdConfiguration.HeaderKey String, String> createHttpRequestHeaders() { ImmutableListMultimap<@CmcdConfiguration.HeaderKey String, String> customData = @@ -264,6 +291,12 @@ public final class CmcdHeadersFactory { if (cmcdConfiguration.isStartupLoggingAllowed()) { cmcdRequest.setStartup(didRebuffer || isBufferEmpty); } + if (cmcdConfiguration.isNextObjectRequestLoggingAllowed()) { + cmcdRequest.setNextObjectRequest(nextObjectRequest); + } + if (cmcdConfiguration.isNextRangeRequestLoggingAllowed()) { + cmcdRequest.setNextRangeRequest(nextRangeRequest); + } if (customData.containsKey(CmcdConfiguration.KEY_CMCD_REQUEST)) { cmcdRequest.setCustomDataList(customData.get(CmcdConfiguration.KEY_CMCD_REQUEST)); } @@ -476,7 +509,7 @@ public final class CmcdHeadersFactory { /** * Keys whose values vary with each request. Contains CMCD fields: {@code bl}, {@code mtp}, {@code - * dl} and {@code su}. + * dl}, {@code su}, {@code nor} and {@code nrr}. */ private static final class CmcdRequest { @@ -486,6 +519,8 @@ public final class CmcdHeadersFactory { private long measuredThroughputInKbps; private long deadlineMs; private boolean startup; + @Nullable private String nextObjectRequest; + @Nullable private String nextRangeRequest; private ImmutableList customDataList; /** Creates a new instance with default values. */ @@ -547,6 +582,23 @@ public final class CmcdHeadersFactory { return this; } + /** + * Sets the {@link CmcdRequest#nextObjectRequest}. This string is URL encoded. The default + * value is {@code null}. + */ + @CanIgnoreReturnValue + public Builder setNextObjectRequest(@Nullable String nextObjectRequest) { + this.nextObjectRequest = nextObjectRequest == null ? null : Uri.encode(nextObjectRequest); + return this; + } + + /** Sets the {@link CmcdRequest#nextRangeRequest}. The default value is {@code null}. */ + @CanIgnoreReturnValue + public Builder setNextRangeRequest(@Nullable String nextRangeRequest) { + this.nextRangeRequest = nextRangeRequest; + return this; + } + /** Sets the {@link CmcdRequest#customDataList}. The default value is an empty list. */ @CanIgnoreReturnValue public Builder setCustomDataList(List customDataList) { @@ -601,6 +653,27 @@ public final class CmcdHeadersFactory { */ public final boolean startup; + /** + * Relative path of the next object to be requested, or {@code null} if unset. This can be used + * to trigger pre-fetching by the CDN. This MUST be a path relative to the current request. + * + *

This string MUST be URL encoded. + * + *

Note: The client SHOULD NOT depend upon any pre-fetch action being taken - it is + * merely a request for such a pre-fetch to take place. + */ + @Nullable public final String nextObjectRequest; + + /** + * The byte range representing the partial object request, or {@code null} if unset. If the + * {@link #nextObjectRequest} field is not set, then the object is assumed to match the object + * currently being requested. + * + *

Note: The client SHOULD NOT depend upon any pre-fetch action being taken - it is + * merely a request for such a pre-fetch to take place. + */ + @Nullable public final String nextRangeRequest; + /** Custom data that vary with each request. */ public final ImmutableList customDataList; @@ -609,6 +682,8 @@ public final class CmcdHeadersFactory { this.measuredThroughputInKbps = builder.measuredThroughputInKbps; this.deadlineMs = builder.deadlineMs; this.startup = builder.startup; + this.nextObjectRequest = builder.nextObjectRequest; + this.nextRangeRequest = builder.nextRangeRequest; this.customDataList = builder.customDataList; } @@ -634,6 +709,16 @@ public final class CmcdHeadersFactory { if (startup) { headerValueList.add(CmcdConfiguration.KEY_STARTUP); } + if (!TextUtils.isEmpty(nextObjectRequest)) { + headerValueList.add( + Util.formatInvariant( + "%s=\"%s\"", CmcdConfiguration.KEY_NEXT_OBJECT_REQUEST, nextObjectRequest)); + } + if (!TextUtils.isEmpty(nextRangeRequest)) { + headerValueList.add( + Util.formatInvariant( + "%s=\"%s\"", CmcdConfiguration.KEY_NEXT_RANGE_REQUEST, nextRangeRequest)); + } headerValueList.addAll(customDataList); if (!headerValueList.isEmpty()) { diff --git a/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DefaultDashChunkSource.java b/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DefaultDashChunkSource.java index 3df13f588b..5352be39a3 100644 --- a/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DefaultDashChunkSource.java +++ b/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DefaultDashChunkSource.java @@ -20,11 +20,13 @@ import static java.lang.Math.min; import android.net.Uri; import android.os.SystemClock; +import android.util.Pair; import androidx.annotation.CheckResult; import androidx.annotation.Nullable; import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.util.UnstableApi; +import androidx.media3.common.util.UriUtil; import androidx.media3.common.util.Util; import androidx.media3.datasource.DataSource; import androidx.media3.datasource.DataSpec; @@ -729,12 +731,17 @@ public class DefaultDashChunkSource implements DashChunkSource { ? 0 : DataSpec.FLAG_MIGHT_NOT_USE_FULL_NETWORK_SPEED; ImmutableMap<@CmcdConfiguration.HeaderKey String, String> httpRequestHeaders = - cmcdHeadersFactory == null - ? ImmutableMap.of() - : cmcdHeadersFactory - .setChunkDurationUs(endTimeUs - startTimeUs) - .setObjectType(CmcdHeadersFactory.getObjectType(trackSelection)) - .createHttpRequestHeaders(); + ImmutableMap.of(); + if (cmcdHeadersFactory != null) { + Pair nextObjectAndRangeRequest = + getNextObjectAndRangeRequest(firstSegmentNum, segmentUri, representationHolder); + cmcdHeadersFactory + .setChunkDurationUs(endTimeUs - startTimeUs) + .setObjectType(CmcdHeadersFactory.getObjectType(trackSelection)) + .setNextObjectRequest(nextObjectAndRangeRequest.first) + .setNextRangeRequest(nextObjectAndRangeRequest.second); + httpRequestHeaders = cmcdHeadersFactory.createHttpRequestHeaders(); + } DataSpec dataSpec = DashUtil.buildDataSpec( representation, @@ -779,12 +786,17 @@ public class DefaultDashChunkSource implements DashChunkSource { ? 0 : DataSpec.FLAG_MIGHT_NOT_USE_FULL_NETWORK_SPEED; ImmutableMap<@CmcdConfiguration.HeaderKey String, String> httpRequestHeaders = - cmcdHeadersFactory == null - ? ImmutableMap.of() - : cmcdHeadersFactory - .setChunkDurationUs(endTimeUs - startTimeUs) - .setObjectType(CmcdHeadersFactory.getObjectType(trackSelection)) - .createHttpRequestHeaders(); + ImmutableMap.of(); + if (cmcdHeadersFactory != null) { + Pair nextObjectAndRangeRequest = + getNextObjectAndRangeRequest(segmentNum, segmentUri, representationHolder); + cmcdHeadersFactory + .setChunkDurationUs(endTimeUs - startTimeUs) + .setObjectType(CmcdHeadersFactory.getObjectType(trackSelection)) + .setNextObjectRequest(nextObjectAndRangeRequest.first) + .setNextRangeRequest(nextObjectAndRangeRequest.second); + httpRequestHeaders = cmcdHeadersFactory.createHttpRequestHeaders(); + } DataSpec dataSpec = DashUtil.buildDataSpec( representation, @@ -810,6 +822,23 @@ public class DefaultDashChunkSource implements DashChunkSource { } } + private Pair getNextObjectAndRangeRequest( + long segmentNum, RangedUri segmentUri, RepresentationHolder representationHolder) { + if (segmentNum + 1 < representationHolder.getSegmentCount()) { + RangedUri nextSegmentUri = representationHolder.getSegmentUrl(segmentNum + 1); + Uri uri = segmentUri.resolveUri(representationHolder.selectedBaseUrl.url); + Uri nextUri = nextSegmentUri.resolveUri(representationHolder.selectedBaseUrl.url); + String nextObjectRequest = UriUtil.getRelativePath(uri, nextUri); + + String nextRangeRequest = nextSegmentUri.start + "-"; + if (nextSegmentUri.length != C.LENGTH_UNSET) { + nextRangeRequest += (nextSegmentUri.start + nextSegmentUri.length); + } + return new Pair<>(nextObjectRequest, nextRangeRequest); + } + return new Pair<>(null, null); + } + private RepresentationHolder updateSelectedBaseUrl(int trackIndex) { RepresentationHolder representationHolder = representationHolders[trackIndex]; @Nullable diff --git a/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DefaultDashChunkSourceTest.java b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DefaultDashChunkSourceTest.java index c6f99ab5c6..1d9895fa2e 100644 --- a/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DefaultDashChunkSourceTest.java +++ b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DefaultDashChunkSourceTest.java @@ -321,7 +321,7 @@ public class DefaultDashChunkSourceTest { "CMCD-Object", "br=700,d=4000,ot=v,tb=1300", "CMCD-Request", - "bl=0,dl=0,mtp=1000,su", + "bl=0,dl=0,mtp=1000,nor=\"..%2Fvideo_4000_700000.m4s\",nrr=\"0-\",su", "CMCD-Session", "cid=\"mediaId\",sf=d,sid=\"" + cmcdConfiguration.sessionId + "\",st=v"); @@ -336,7 +336,7 @@ public class DefaultDashChunkSourceTest { "CMCD-Object", "br=700,d=4000,ot=v,tb=1300", "CMCD-Request", - "bl=1000,dl=800,mtp=1000", + "bl=1000,dl=800,mtp=1000,nor=\"..%2Fvideo_8000_700000.m4s\",nrr=\"0-\"", "CMCD-Session", "cid=\"mediaId\",pr=1.25,sf=d,sid=\"" + cmcdConfiguration.sessionId + "\",st=v"); } @@ -420,7 +420,7 @@ public class DefaultDashChunkSourceTest { "CMCD-Object", "br=700,d=4000,ot=v,tb=1300", "CMCD-Request", - "bl=0,dl=0,mtp=1000,su", + "bl=0,dl=0,mtp=1000,nor=\"..%2Fvideo_4000_700000.m4s\",nrr=\"0-\",su", "CMCD-Session", "cid=\"mediaIdcontentIdSuffix\",sf=d,st=v", "CMCD-Status", @@ -468,7 +468,7 @@ public class DefaultDashChunkSourceTest { "CMCD-Object", "br=700,d=4000,key-1=1,ot=v,tb=1300", "CMCD-Request", - "bl=0,dl=0,key-2=\"stringValue\",mtp=1000,su", + "bl=0,dl=0,key-2=\"stringValue\",mtp=1000,nor=\"..%2Fvideo_4000_700000.m4s\",nrr=\"0-\",su", "CMCD-Session", "cid=\"mediaId\",key-3=3,sf=d,sid=\"" + cmcdConfiguration.sessionId + "\",st=v", "CMCD-Status", diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsChunkSource.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsChunkSource.java index fd8a19b278..908a848c1e 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsChunkSource.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsChunkSource.java @@ -488,23 +488,44 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; seenExpectedPlaylistError = false; expectedPlaylistUrl = null; - @Nullable - CmcdHeadersFactory cmcdHeadersFactory = - cmcdConfiguration == null - ? null - : new CmcdHeadersFactory( - cmcdConfiguration, - trackSelection, - bufferedDurationUs, - /* playbackRate= */ loadingInfo.playbackSpeed, - /* streamingFormat= */ CmcdHeadersFactory.STREAMING_FORMAT_HLS, - /* isLive= */ !playlist.hasEndTag, - /* didRebuffer= */ loadingInfo.rebufferedSince(lastChunkRequestRealtimeMs), - /* isBufferEmpty= */ queue.isEmpty()) - .setObjectType( - getIsMuxedAudioAndVideo() - ? CmcdHeadersFactory.OBJECT_TYPE_MUXED_AUDIO_AND_VIDEO - : CmcdHeadersFactory.getObjectType(trackSelection)); + @Nullable CmcdHeadersFactory cmcdHeadersFactory = null; + if (cmcdConfiguration != null) { + cmcdHeadersFactory = + new CmcdHeadersFactory( + cmcdConfiguration, + trackSelection, + bufferedDurationUs, + /* playbackRate= */ loadingInfo.playbackSpeed, + /* streamingFormat= */ CmcdHeadersFactory.STREAMING_FORMAT_HLS, + /* isLive= */ !playlist.hasEndTag, + /* didRebuffer= */ loadingInfo.rebufferedSince(lastChunkRequestRealtimeMs), + /* isBufferEmpty= */ queue.isEmpty()) + .setObjectType( + getIsMuxedAudioAndVideo() + ? CmcdHeadersFactory.OBJECT_TYPE_MUXED_AUDIO_AND_VIDEO + : CmcdHeadersFactory.getObjectType(trackSelection)); + + long nextChunkMediaSequence = + partIndex == C.LENGTH_UNSET + ? (chunkMediaSequence == C.LENGTH_UNSET ? C.LENGTH_UNSET : chunkMediaSequence + 1) + : chunkMediaSequence; + int nextPartIndex = partIndex == C.LENGTH_UNSET ? C.LENGTH_UNSET : partIndex + 1; + SegmentBaseHolder nextSegmentBaseHolder = + getNextSegmentHolder(playlist, nextChunkMediaSequence, nextPartIndex); + if (nextSegmentBaseHolder != null) { + Uri uri = UriUtil.resolveToUri(playlist.baseUri, segmentBaseHolder.segmentBase.url); + Uri nextUri = UriUtil.resolveToUri(playlist.baseUri, nextSegmentBaseHolder.segmentBase.url); + cmcdHeadersFactory.setNextObjectRequest(UriUtil.getRelativePath(uri, nextUri)); + + String nextRangeRequest = nextSegmentBaseHolder.segmentBase.byteRangeOffset + "-"; + if (nextSegmentBaseHolder.segmentBase.byteRangeLength != C.LENGTH_UNSET) { + nextRangeRequest += + (nextSegmentBaseHolder.segmentBase.byteRangeOffset + + nextSegmentBaseHolder.segmentBase.byteRangeLength); + } + cmcdHeadersFactory.setNextRangeRequest(nextRangeRequest); + } + } lastChunkRequestRealtimeMs = SystemClock.elapsedRealtime(); // Check if the media segment or its initialization segment are fully encrypted. diff --git a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsChunkSourceTest.java b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsChunkSourceTest.java index b8e52dee7c..87d4aec93e 100644 --- a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsChunkSourceTest.java +++ b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsChunkSourceTest.java @@ -216,7 +216,7 @@ public class HlsChunkSourceTest { "CMCD-Object", "br=800,d=4000,ot=v,tb=800", "CMCD-Request", - "bl=0,dl=0,su", + "bl=0,dl=0,nor=\"..%2F3.mp4\",nrr=\"0-\",su", "CMCD-Session", "cid=\"mediaId\",sf=h,sid=\"" + cmcdConfiguration.sessionId + "\",st=v"); @@ -232,7 +232,7 @@ public class HlsChunkSourceTest { "CMCD-Object", "br=800,d=4000,ot=v,tb=800", "CMCD-Request", - "bl=1000,dl=800", + "bl=1000,dl=800,nor=\"..%2F3.mp4\",nrr=\"0-\"", "CMCD-Session", "cid=\"mediaId\",pr=1.25,sf=h,sid=\"" + cmcdConfiguration.sessionId + "\",st=v"); } @@ -329,7 +329,7 @@ public class HlsChunkSourceTest { "CMCD-Object", "br=800,d=4000,ot=v,tb=800", "CMCD-Request", - "bl=0,dl=0,su", + "bl=0,dl=0,nor=\"..%2F3.mp4\",nrr=\"0-\",su", "CMCD-Session", "cid=\"mediaIdcontentIdSuffix\",sf=h,st=v", "CMCD-Status", @@ -378,7 +378,7 @@ public class HlsChunkSourceTest { "CMCD-Object", "br=800,d=4000,key-1=1,ot=v,tb=800", "CMCD-Request", - "bl=0,dl=0,key-2=\"stringValue\",su", + "bl=0,dl=0,key-2=\"stringValue\",nor=\"..%2F3.mp4\",nrr=\"0-\",su", "CMCD-Session", "cid=\"mediaId\",key-3=3,sf=h,sid=\"" + cmcdConfiguration.sessionId + "\",st=v", "CMCD-Status", diff --git a/libraries/exoplayer_smoothstreaming/src/main/java/androidx/media3/exoplayer/smoothstreaming/DefaultSsChunkSource.java b/libraries/exoplayer_smoothstreaming/src/main/java/androidx/media3/exoplayer/smoothstreaming/DefaultSsChunkSource.java index 63adf0848d..7e4e51dae2 100644 --- a/libraries/exoplayer_smoothstreaming/src/main/java/androidx/media3/exoplayer/smoothstreaming/DefaultSsChunkSource.java +++ b/libraries/exoplayer_smoothstreaming/src/main/java/androidx/media3/exoplayer/smoothstreaming/DefaultSsChunkSource.java @@ -24,6 +24,7 @@ import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.util.Assertions; import androidx.media3.common.util.UnstableApi; +import androidx.media3.common.util.UriUtil; import androidx.media3.datasource.DataSource; import androidx.media3.datasource.DataSpec; import androidx.media3.datasource.TransferListener; @@ -290,21 +291,26 @@ public class DefaultSsChunkSource implements SsChunkSource { int manifestTrackIndex = trackSelection.getIndexInTrackGroup(trackSelectionIndex); Uri uri = streamElement.buildRequestUri(manifestTrackIndex, chunkIndex); - @Nullable - CmcdHeadersFactory cmcdHeadersFactory = - cmcdConfiguration == null - ? null - : new CmcdHeadersFactory( - cmcdConfiguration, - trackSelection, - bufferedDurationUs, - /* playbackRate= */ loadingInfo.playbackSpeed, - /* streamingFormat= */ CmcdHeadersFactory.STREAMING_FORMAT_SS, - /* isLive= */ manifest.isLive, - /* didRebuffer= */ loadingInfo.rebufferedSince(lastChunkRequestRealtimeMs), - /* isBufferEmpty= */ queue.isEmpty()) - .setChunkDurationUs(chunkEndTimeUs - chunkStartTimeUs) - .setObjectType(CmcdHeadersFactory.getObjectType(trackSelection)); + @Nullable CmcdHeadersFactory cmcdHeadersFactory = null; + if (cmcdConfiguration != null) { + cmcdHeadersFactory = + new CmcdHeadersFactory( + cmcdConfiguration, + trackSelection, + bufferedDurationUs, + /* playbackRate= */ loadingInfo.playbackSpeed, + /* streamingFormat= */ CmcdHeadersFactory.STREAMING_FORMAT_SS, + /* isLive= */ manifest.isLive, + /* didRebuffer= */ loadingInfo.rebufferedSince(lastChunkRequestRealtimeMs), + /* isBufferEmpty= */ queue.isEmpty()) + .setChunkDurationUs(chunkEndTimeUs - chunkStartTimeUs) + .setObjectType(CmcdHeadersFactory.getObjectType(trackSelection)); + + if (chunkIndex + 1 < streamElement.chunkCount) { + Uri nextUri = streamElement.buildRequestUri(manifestTrackIndex, chunkIndex + 1); + cmcdHeadersFactory.setNextObjectRequest(UriUtil.getRelativePath(uri, nextUri)); + } + } lastChunkRequestRealtimeMs = SystemClock.elapsedRealtime(); out.chunk = diff --git a/libraries/exoplayer_smoothstreaming/src/test/java/androidx/media3/exoplayer/smoothstreaming/DefaultSsChunkSourceTest.java b/libraries/exoplayer_smoothstreaming/src/test/java/androidx/media3/exoplayer/smoothstreaming/DefaultSsChunkSourceTest.java index 923c0dd22b..075ab15309 100644 --- a/libraries/exoplayer_smoothstreaming/src/test/java/androidx/media3/exoplayer/smoothstreaming/DefaultSsChunkSourceTest.java +++ b/libraries/exoplayer_smoothstreaming/src/test/java/androidx/media3/exoplayer/smoothstreaming/DefaultSsChunkSourceTest.java @@ -71,7 +71,7 @@ public class DefaultSsChunkSourceTest { "CMCD-Object", "br=308,d=1968,ot=v,tb=1536", "CMCD-Request", - "bl=0,dl=0,mtp=1000,su", + "bl=0,dl=0,mtp=1000,nor=\"..%2FFragments(video%3D19680000)\",su", "CMCD-Session", "cid=\"mediaId\",sf=s,sid=\"" + cmcdConfiguration.sessionId + "\",st=v"); @@ -86,7 +86,7 @@ public class DefaultSsChunkSourceTest { "CMCD-Object", "br=308,d=898,ot=v,tb=1536", "CMCD-Request", - "bl=1000,dl=500,mtp=1000", + "bl=1000,dl=500,mtp=1000,nor=\"..%2FFragments(video%3D28660000)\"", "CMCD-Session", "cid=\"mediaId\",pr=2.00,sf=s,sid=\"" + cmcdConfiguration.sessionId + "\",st=v"); } @@ -170,7 +170,7 @@ public class DefaultSsChunkSourceTest { "CMCD-Object", "br=308,d=1968,ot=v,tb=1536", "CMCD-Request", - "bl=0,dl=0,mtp=1000,su", + "bl=0,dl=0,mtp=1000,nor=\"..%2FFragments(video%3D19680000)\",su", "CMCD-Session", "cid=\"mediaIdcontentIdSuffix\",sf=s,st=v", "CMCD-Status", @@ -218,7 +218,7 @@ public class DefaultSsChunkSourceTest { "CMCD-Object", "br=308,d=1968,key-1=1,ot=v,tb=1536", "CMCD-Request", - "bl=0,dl=0,key-2=\"stringValue\",mtp=1000,su", + "bl=0,dl=0,key-2=\"stringValue\",mtp=1000,nor=\"..%2FFragments(video%3D19680000)\",su", "CMCD-Session", "cid=\"mediaId\",key-3=3,sf=s,sid=\"" + cmcdConfiguration.sessionId + "\",st=v", "CMCD-Status", diff --git a/libraries/test_data/src/test/assets/media/smooth-streaming/sample_ismc_1 b/libraries/test_data/src/test/assets/media/smooth-streaming/sample_ismc_1 index 1d279d0a67..1254761cb3 100644 --- a/libraries/test_data/src/test/assets/media/smooth-streaming/sample_ismc_1 +++ b/libraries/test_data/src/test/assets/media/smooth-streaming/sample_ismc_1 @@ -39,6 +39,7 @@ +