diff --git a/RELEASENOTES.md b/RELEASENOTES.md
index d552dc269c..fff06b960d 100644
--- a/RELEASENOTES.md
+++ b/RELEASENOTES.md
@@ -27,6 +27,9 @@
* `RenderersFactory.createSecondaryRenderer` can be implemented to provide
secondary renderers for pre-warming. Pre-warming enables quicker media
item transitions during playback.
+ * Enable sending `CmcdData` for manifest requests in adaptive streaming
+ formats DASH, HLS, and SmoothStreaming
+ ([#1951](https://github.com/androidx/media/issues/1951)).
* Transformer:
* Update parameters of `VideoFrameProcessor.registerInputStream` and
`VideoFrameProcessor.Listener.onInputStreamRegistered` to use `Format`.
diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/CmcdData.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/CmcdData.java
index 3ec0717f01..3c8151badb 100644
--- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/CmcdData.java
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/CmcdData.java
@@ -16,7 +16,9 @@
package androidx.media3.exoplayer.upstream;
import static androidx.media3.common.util.Assertions.checkArgument;
+import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState;
+import static androidx.media3.common.util.Assertions.checkStateNotNull;
import static java.lang.Math.max;
import static java.lang.annotation.ElementType.TYPE_USE;
@@ -46,6 +48,7 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
+import java.util.Objects;
import java.util.regex.Pattern;
/**
@@ -89,6 +92,9 @@ public final class CmcdData {
/** Represents the object type for muxed audio and video content in a media container. */
public static final String OBJECT_TYPE_MUXED_AUDIO_AND_VIDEO = "av";
+ /** Represents the object type for a manifest or playlist file, in a media container. */
+ public static final String OBJECT_TYPE_MANIFEST = "m";
+
/**
* Custom key names MUST carry a hyphenated prefix to ensure that there will not be a namespace
* collision with future revisions to this specification. Clients SHOULD use a reverse-DNS
@@ -97,13 +103,13 @@ public final class CmcdData {
private static final Pattern CUSTOM_KEY_NAME_PATTERN = Pattern.compile(".*-.*");
private final CmcdConfiguration cmcdConfiguration;
- private final ExoTrackSelection trackSelection;
- private final long bufferedDurationUs;
- private final float playbackRate;
private final @CmcdData.StreamingFormat String streamingFormat;
- private final boolean isLive;
- private final boolean didRebuffer;
- private final boolean isBufferEmpty;
+ @Nullable private ExoTrackSelection trackSelection;
+ private long bufferedDurationUs;
+ private float playbackRate;
+ @Nullable private Boolean isLive;
+ private boolean didRebuffer;
+ private boolean isBufferEmpty;
private long chunkDurationUs;
@Nullable private @CmcdData.ObjectType String objectType;
@Nullable private String nextObjectRequest;
@@ -112,41 +118,15 @@ public final class CmcdData {
/**
* Creates an instance.
*
- * @param cmcdConfiguration The {@link CmcdConfiguration} for this chunk source.
- * @param trackSelection The {@linkplain ExoTrackSelection track selection}.
- * @param bufferedDurationUs The duration of media currently buffered from the current playback
- * position, in microseconds.
- * @param playbackRate The playback rate indicating the current speed of playback.
- * @param streamingFormat The streaming format of the media content. Must be one of the allowed
- * streaming formats specified by the {@link CmcdData.StreamingFormat} annotation.
- * @param isLive {@code true} if the media content is being streamed live, {@code false}
- * otherwise.
- * @param didRebuffer {@code true} if a rebuffering event happened between the previous request
- * and this one, {@code false} otherwise.
- * @param isBufferEmpty {@code true} if the queue of buffered chunks is empty, {@code false}
- * otherwise.
- * @throws IllegalArgumentException If {@code bufferedDurationUs} is negative or {@code
- * playbackRate} is non-positive.
+ * @param cmcdConfiguration The {@link CmcdConfiguration} for this source.
+ * @param streamingFormat The streaming format of the media content.
*/
public Factory(
- CmcdConfiguration cmcdConfiguration,
- ExoTrackSelection trackSelection,
- long bufferedDurationUs,
- float playbackRate,
- @CmcdData.StreamingFormat String streamingFormat,
- boolean isLive,
- boolean didRebuffer,
- boolean isBufferEmpty) {
- checkArgument(bufferedDurationUs >= 0);
- checkArgument(playbackRate == C.RATE_UNSET || playbackRate > 0);
+ CmcdConfiguration cmcdConfiguration, @CmcdData.StreamingFormat String streamingFormat) {
this.cmcdConfiguration = cmcdConfiguration;
- this.trackSelection = trackSelection;
- this.bufferedDurationUs = bufferedDurationUs;
- this.playbackRate = playbackRate;
+ this.bufferedDurationUs = C.TIME_UNSET;
+ this.playbackRate = C.RATE_UNSET;
this.streamingFormat = streamingFormat;
- this.isLive = isLive;
- this.didRebuffer = didRebuffer;
- this.isBufferEmpty = isBufferEmpty;
this.chunkDurationUs = C.TIME_UNSET;
}
@@ -160,7 +140,6 @@ public final class CmcdData {
*/
@Nullable
public static @CmcdData.ObjectType String getObjectType(ExoTrackSelection trackSelection) {
- checkArgument(trackSelection != null);
@TrackType
int trackType = MimeTypes.getTrackType(trackSelection.getSelectedFormat().sampleMimeType);
if (trackType == C.TRACK_TYPE_UNKNOWN) {
@@ -178,8 +157,13 @@ public final class CmcdData {
}
/**
- * Sets the duration of current media chunk being requested, in microseconds. The default value
- * is {@link C#TIME_UNSET}.
+ * Sets the duration of current media chunk being requested, in microseconds.
+ *
+ *
Must be set to a non-negative value if the {@linkplain #setObjectType(String) object type}
+ * is set and one of {@link #OBJECT_TYPE_AUDIO_ONLY}, {@link #OBJECT_TYPE_VIDEO_ONLY} or {@link
+ * #OBJECT_TYPE_MUXED_AUDIO_AND_VIDEO}.
+ *
+ *
Default value is {@link C#TIME_UNSET}.
*
* @throws IllegalArgumentException If {@code chunkDurationUs} is negative.
*/
@@ -191,8 +175,7 @@ public final class CmcdData {
}
/**
- * Sets the object type of the current object being requested. Must be one of the allowed object
- * types specified by the {@link CmcdData.ObjectType} annotation.
+ * Sets the object type of the current object being requested.
*
*
Default is {@code null}.
*/
@@ -226,31 +209,145 @@ public final class CmcdData {
return this;
}
+ /**
+ * Sets the {@linkplain ExoTrackSelection track selection} for the media being played.
+ *
+ *
Must be set to a non-null value if the {@link #setObjectType(String)} is not {@link
+ * #OBJECT_TYPE_MANIFEST}
+ *
+ *
Default is {@code null}.
+ */
+ @CanIgnoreReturnValue
+ public Factory setTrackSelection(ExoTrackSelection trackSelection) {
+ this.trackSelection = trackSelection;
+ return this;
+ }
+
+ /**
+ * Sets the duration of media currently buffered from the current playback position, in
+ * microseconds.
+ *
+ *
Must be set to a non-negative value if the {@linkplain #setObjectType(String) object type}
+ * is set and one of {@link #OBJECT_TYPE_AUDIO_ONLY}, {@link #OBJECT_TYPE_VIDEO_ONLY} or {@link
+ * #OBJECT_TYPE_MUXED_AUDIO_AND_VIDEO}.
+ *
+ *
Default value is {@link C#TIME_UNSET}.
+ *
+ * @throws IllegalArgumentException If {@code bufferedDurationUs} is negative.
+ */
+ @CanIgnoreReturnValue
+ public Factory setBufferedDurationUs(long bufferedDurationUs) {
+ checkArgument(bufferedDurationUs >= 0);
+ this.bufferedDurationUs = bufferedDurationUs;
+ return this;
+ }
+
+ /**
+ * Sets the playback rate indicating the current speed of playback.
+ *
+ *
Default value is {@link C#RATE_UNSET}.
+ *
+ * @throws IllegalArgumentException If {@code playbackRate} is non-positive and not {@link
+ * C#RATE_UNSET}.
+ */
+ @CanIgnoreReturnValue
+ public Factory setPlaybackRate(float playbackRate) {
+ checkArgument(playbackRate == C.RATE_UNSET || playbackRate > 0);
+ this.playbackRate = playbackRate;
+ return this;
+ }
+
+ /**
+ * Sets whether the media content is being streamed live.
+ *
+ *
Default value is {@code false}.
+ */
+ @CanIgnoreReturnValue
+ public Factory setIsLive(boolean isLive) {
+ this.isLive = isLive;
+ return this;
+ }
+
+ /**
+ * Sets whether a rebuffering event occurred between the previous request and this one.
+ *
+ *
Default value is {@code false}.
+ */
+ @CanIgnoreReturnValue
+ public Factory setDidRebuffer(boolean didRebuffer) {
+ this.didRebuffer = didRebuffer;
+ return this;
+ }
+
+ /**
+ * Sets whether the queue of buffered chunks is empty.
+ *
+ *
Default value is {@code false}.
+ */
+ @CanIgnoreReturnValue
+ public Factory setIsBufferEmpty(boolean isBufferEmpty) {
+ this.isBufferEmpty = isBufferEmpty;
+ return this;
+ }
+
+ /**
+ * Creates a {@link CmcdData} instance.
+ *
+ * @throws IllegalStateException If any required parameters have not been set.
+ */
public CmcdData createCmcdData() {
+ boolean isManifestObjectType = isManifestObjectType(objectType);
+ boolean isMediaObjectType = isMediaObjectType(objectType);
+
+ if (!isManifestObjectType) {
+ checkStateNotNull(trackSelection, "Track selection must be set");
+ }
+ if (isMediaObjectType) {
+ checkState(bufferedDurationUs != C.TIME_UNSET, "Buffered duration must be set");
+ checkState(chunkDurationUs != C.TIME_UNSET, "Chunk duration must be set");
+ }
+
ImmutableListMultimap<@CmcdConfiguration.HeaderKey String, String> customData =
cmcdConfiguration.requestConfig.getCustomData();
for (String headerKey : customData.keySet()) {
validateCustomDataListFormat(customData.get(headerKey));
}
- int bitrateKbps = Util.ceilDivide(trackSelection.getSelectedFormat().bitrate, 1000);
+ int bitrateKbps = C.RATE_UNSET_INT;
+ int topBitrateKbps = C.RATE_UNSET_INT;
+ long latestBitrateEstimateKbps = C.RATE_UNSET_INT;
+ int requestedMaximumThroughputKbps = C.RATE_UNSET_INT;
+
+ if (!isManifestObjectType) {
+ ExoTrackSelection trackSelection = checkNotNull(this.trackSelection);
+ int selectedTrackBitrate = trackSelection.getSelectedFormat().bitrate;
+ bitrateKbps = Util.ceilDivide(selectedTrackBitrate, 1000);
+
+ TrackGroup trackGroup = trackSelection.getTrackGroup();
+ int topBitrate = selectedTrackBitrate;
+ for (int i = 0; i < trackGroup.length; i++) {
+ topBitrate = max(topBitrate, trackGroup.getFormat(i).bitrate);
+ }
+ topBitrateKbps = Util.ceilDivide(topBitrate, 1000);
+
+ if (trackSelection.getLatestBitrateEstimate() != C.RATE_UNSET_INT) {
+ latestBitrateEstimateKbps =
+ Util.ceilDivide(trackSelection.getLatestBitrateEstimate(), 1000);
+ }
+
+ requestedMaximumThroughputKbps =
+ cmcdConfiguration.requestConfig.getRequestedMaximumThroughputKbps(bitrateKbps);
+ }
CmcdObject.Builder cmcdObject = new CmcdObject.Builder();
- if (!getIsInitSegment()) {
- if (cmcdConfiguration.isBitrateLoggingAllowed()) {
- cmcdObject.setBitrateKbps(bitrateKbps);
- }
- if (cmcdConfiguration.isTopBitrateLoggingAllowed()) {
- TrackGroup trackGroup = trackSelection.getTrackGroup();
- int topBitrate = trackSelection.getSelectedFormat().bitrate;
- for (int i = 0; i < trackGroup.length; i++) {
- topBitrate = max(topBitrate, trackGroup.getFormat(i).bitrate);
- }
- cmcdObject.setTopBitrateKbps(Util.ceilDivide(topBitrate, 1000));
- }
- if (cmcdConfiguration.isObjectDurationLoggingAllowed()) {
- cmcdObject.setObjectDurationMs(Util.usToMs(chunkDurationUs));
- }
+ if (cmcdConfiguration.isBitrateLoggingAllowed()) {
+ cmcdObject.setBitrateKbps(bitrateKbps);
+ }
+ if (cmcdConfiguration.isTopBitrateLoggingAllowed()) {
+ cmcdObject.setTopBitrateKbps(topBitrateKbps);
+ }
+ if (isMediaObjectType && cmcdConfiguration.isObjectDurationLoggingAllowed()) {
+ cmcdObject.setObjectDurationMs(Util.usToMs(chunkDurationUs));
}
if (cmcdConfiguration.isObjectTypeLoggingAllowed()) {
cmcdObject.setObjectType(objectType);
@@ -260,16 +357,16 @@ public final class CmcdData {
}
CmcdRequest.Builder cmcdRequest = new CmcdRequest.Builder();
- if (!getIsInitSegment() && cmcdConfiguration.isBufferLengthLoggingAllowed()) {
- cmcdRequest.setBufferLengthMs(Util.usToMs(bufferedDurationUs));
+ if (isMediaObjectType) {
+ if (cmcdConfiguration.isBufferLengthLoggingAllowed()) {
+ cmcdRequest.setBufferLengthMs(Util.usToMs(bufferedDurationUs));
+ }
+ if (cmcdConfiguration.isDeadlineLoggingAllowed()) {
+ cmcdRequest.setDeadlineMs(Util.usToMs((long) (bufferedDurationUs / playbackRate)));
+ }
}
- if (cmcdConfiguration.isMeasuredThroughputLoggingAllowed()
- && trackSelection.getLatestBitrateEstimate() != C.RATE_UNSET_INT) {
- cmcdRequest.setMeasuredThroughputInKbps(
- Util.ceilDivide(trackSelection.getLatestBitrateEstimate(), 1000));
- }
- if (cmcdConfiguration.isDeadlineLoggingAllowed()) {
- cmcdRequest.setDeadlineMs(Util.usToMs((long) (bufferedDurationUs / playbackRate)));
+ if (cmcdConfiguration.isMeasuredThroughputLoggingAllowed()) {
+ cmcdRequest.setMeasuredThroughputInKbps(latestBitrateEstimateKbps);
}
if (cmcdConfiguration.isStartupLoggingAllowed()) {
cmcdRequest.setStartup(didRebuffer || isBufferEmpty);
@@ -294,8 +391,8 @@ public final class CmcdData {
if (cmcdConfiguration.isStreamingFormatLoggingAllowed()) {
cmcdSession.setStreamingFormat(streamingFormat);
}
- if (cmcdConfiguration.isStreamTypeLoggingAllowed()) {
- cmcdSession.setStreamType(isLive ? STREAM_TYPE_LIVE : STREAM_TYPE_VOD);
+ if (isLive != null && cmcdConfiguration.isStreamTypeLoggingAllowed()) {
+ cmcdSession.setStreamType(checkNotNull(isLive) ? STREAM_TYPE_LIVE : STREAM_TYPE_VOD);
}
if (cmcdConfiguration.isPlaybackRateLoggingAllowed()) {
cmcdSession.setPlaybackRate(playbackRate);
@@ -306,8 +403,7 @@ public final class CmcdData {
CmcdStatus.Builder cmcdStatus = new CmcdStatus.Builder();
if (cmcdConfiguration.isMaximumRequestThroughputLoggingAllowed()) {
- cmcdStatus.setMaximumRequestedThroughputKbps(
- cmcdConfiguration.requestConfig.getRequestedMaximumThroughputKbps(bitrateKbps));
+ cmcdStatus.setMaximumRequestedThroughputKbps(requestedMaximumThroughputKbps);
}
if (cmcdConfiguration.isBufferStarvationLoggingAllowed()) {
cmcdStatus.setBufferStarvation(didRebuffer);
@@ -324,8 +420,14 @@ public final class CmcdData {
cmcdConfiguration.dataTransmissionMode);
}
- private boolean getIsInitSegment() {
- return objectType != null && objectType.equals(OBJECT_TYPE_INIT_SEGMENT);
+ private static boolean isManifestObjectType(@Nullable @ObjectType String objectType) {
+ return Objects.equals(objectType, Factory.OBJECT_TYPE_MANIFEST);
+ }
+
+ private static boolean isMediaObjectType(@Nullable @ObjectType String objectType) {
+ return Objects.equals(objectType, Factory.OBJECT_TYPE_AUDIO_ONLY)
+ || Objects.equals(objectType, Factory.OBJECT_TYPE_VIDEO_ONLY)
+ || Objects.equals(objectType, Factory.OBJECT_TYPE_MUXED_AUDIO_AND_VIDEO);
}
private void validateCustomDataListFormat(List customDataList) {
@@ -360,7 +462,8 @@ public final class CmcdData {
Factory.OBJECT_TYPE_INIT_SEGMENT,
Factory.OBJECT_TYPE_AUDIO_ONLY,
Factory.OBJECT_TYPE_VIDEO_ONLY,
- Factory.OBJECT_TYPE_MUXED_AUDIO_AND_VIDEO
+ Factory.OBJECT_TYPE_MUXED_AUDIO_AND_VIDEO,
+ Factory.OBJECT_TYPE_MANIFEST
})
@Documented
@Target(TYPE_USE)
@@ -528,10 +631,8 @@ public final class CmcdData {
public final long objectDurationMs;
/**
- * The media type of the current object being requested , or {@code null} if unset. Must be one
- * of the allowed object types specified by the {@link ObjectType} annotation.
- *
- * If the object type being requested is unknown, then this key MUST NOT be used.
+ * The media type of the current object being requested. Must be one of the allowed object types
+ * specified by the {@link ObjectType} annotation.
*/
@Nullable public final @ObjectType String objectType;
@@ -607,8 +708,13 @@ public final class CmcdData {
*/
@CanIgnoreReturnValue
public Builder setBufferLengthMs(long bufferLengthMs) {
- checkArgument(bufferLengthMs >= 0 || bufferLengthMs == C.TIME_UNSET);
- this.bufferLengthMs = ((bufferLengthMs + 50) / 100) * 100;
+ if (bufferLengthMs == C.TIME_UNSET) {
+ this.bufferLengthMs = bufferLengthMs;
+ } else if (bufferLengthMs >= 0) {
+ this.bufferLengthMs = ((bufferLengthMs + 50) / 100) * 100;
+ } else {
+ throw new IllegalArgumentException();
+ }
return this;
}
@@ -621,10 +727,13 @@ public final class CmcdData {
*/
@CanIgnoreReturnValue
public Builder setMeasuredThroughputInKbps(long measuredThroughputInKbps) {
- checkArgument(
- measuredThroughputInKbps >= 0 || measuredThroughputInKbps == C.RATE_UNSET_INT);
- this.measuredThroughputInKbps = ((measuredThroughputInKbps + 50) / 100) * 100;
-
+ if (measuredThroughputInKbps == C.RATE_UNSET_INT) {
+ this.measuredThroughputInKbps = measuredThroughputInKbps;
+ } else if (measuredThroughputInKbps >= 0) {
+ this.measuredThroughputInKbps = ((measuredThroughputInKbps + 50) / 100) * 100;
+ } else {
+ throw new IllegalArgumentException();
+ }
return this;
}
@@ -637,8 +746,13 @@ public final class CmcdData {
*/
@CanIgnoreReturnValue
public Builder setDeadlineMs(long deadlineMs) {
- checkArgument(deadlineMs >= 0 || deadlineMs == C.TIME_UNSET);
- this.deadlineMs = ((deadlineMs + 50) / 100) * 100;
+ if (deadlineMs == C.TIME_UNSET) {
+ this.deadlineMs = deadlineMs;
+ } else if (deadlineMs >= 0) {
+ this.deadlineMs = ((deadlineMs + 50) / 100) * 100;
+ } else {
+ throw new IllegalArgumentException();
+ }
return this;
}
diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/CmcdDataTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/CmcdDataTest.java
index bafe3e352b..65ff949074 100644
--- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/CmcdDataTest.java
+++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/CmcdDataTest.java
@@ -36,23 +36,48 @@ import org.junit.runner.RunWith;
public class CmcdDataTest {
@Test
- public void createInstance_populatesCmcdHttRequestHeaders() {
+ public void createInstance_withInvalidFactoryState_throwsIllegalStateException() {
+ CmcdConfiguration cmcdConfiguration =
+ CmcdConfiguration.Factory.DEFAULT.createCmcdConfiguration(MediaItem.EMPTY);
+ ExoTrackSelection trackSelection = mock(ExoTrackSelection.class);
+
+ assertThrows(
+ "Track selection must be set",
+ IllegalStateException.class,
+ () ->
+ new CmcdData.Factory(cmcdConfiguration, CmcdData.Factory.STREAMING_FORMAT_DASH)
+ .setObjectType(CmcdData.Factory.OBJECT_TYPE_INIT_SEGMENT)
+ .createCmcdData());
+
+ assertThrows(
+ "Buffered duration must be set",
+ IllegalStateException.class,
+ () ->
+ new CmcdData.Factory(cmcdConfiguration, CmcdData.Factory.STREAMING_FORMAT_DASH)
+ .setObjectType(CmcdData.Factory.OBJECT_TYPE_AUDIO_ONLY)
+ .setTrackSelection(trackSelection)
+ .setChunkDurationUs(100_000)
+ .createCmcdData());
+
+ assertThrows(
+ "Chunk duration must be set",
+ IllegalStateException.class,
+ () ->
+ new CmcdData.Factory(cmcdConfiguration, CmcdData.Factory.STREAMING_FORMAT_DASH)
+ .setObjectType(CmcdData.Factory.OBJECT_TYPE_AUDIO_ONLY)
+ .setTrackSelection(trackSelection)
+ .setBufferedDurationUs(100_000)
+ .createCmcdData());
+ }
+
+ @Test
+ public void createInstance_audioObjectType_setsCorrectHttpHeaders() {
CmcdConfiguration.Factory cmcdConfigurationFactory =
mediaItem ->
new CmcdConfiguration(
"sessionId",
mediaItem.mediaId,
new CmcdConfiguration.RequestConfig() {
- @Override
- public ImmutableListMultimap<@CmcdConfiguration.HeaderKey String, String>
- getCustomData() {
- return new ImmutableListMultimap.Builder()
- .putAll("CMCD-Object", "key-1=1", "key-2-separated-by-multiple-hyphens=2")
- .put("CMCD-Request", "key-3=\"stringValue1,stringValue2\"")
- .put("CMCD-Status", "key-4=\"stringValue3=stringValue4\"")
- .build();
- }
-
@Override
public int getRequestedMaximumThroughputKbps(int throughputKbps) {
return 2 * throughputKbps;
@@ -69,15 +94,14 @@ public class CmcdDataTest {
when(trackSelection.getLatestBitrateEstimate()).thenReturn(500_000L);
DataSpec dataSpec = new DataSpec.Builder().setUri(Uri.EMPTY).build();
CmcdData cmcdData =
- new CmcdData.Factory(
- cmcdConfiguration,
- trackSelection,
- /* bufferedDurationUs= */ 1_760_000,
- /* playbackRate= */ 2.0f,
- /* streamingFormat= */ CmcdData.Factory.STREAMING_FORMAT_DASH,
- /* isLive= */ true,
- /* didRebuffer= */ true,
- /* isBufferEmpty= */ false)
+ new CmcdData.Factory(cmcdConfiguration, CmcdData.Factory.STREAMING_FORMAT_DASH)
+ .setTrackSelection(trackSelection)
+ .setObjectType(CmcdData.Factory.OBJECT_TYPE_AUDIO_ONLY)
+ .setBufferedDurationUs(1_760_000)
+ .setPlaybackRate(2.0f)
+ .setIsLive(true)
+ .setDidRebuffer(true)
+ .setIsBufferEmpty(false)
.setChunkDurationUs(3_000_000)
.createCmcdData();
@@ -86,32 +110,23 @@ public class CmcdDataTest {
assertThat(dataSpec.httpRequestHeaders)
.containsExactly(
"CMCD-Object",
- "br=840,d=3000,key-1=1,key-2-separated-by-multiple-hyphens=2,tb=1000",
+ "br=840,d=3000,ot=a,tb=1000",
"CMCD-Request",
- "bl=1800,dl=900,key-3=\"stringValue1,stringValue2\",mtp=500,su",
+ "bl=1800,dl=900,mtp=500,su",
"CMCD-Session",
"cid=\"mediaId\",pr=2.00,sf=d,sid=\"sessionId\",st=l",
"CMCD-Status",
- "bs,key-4=\"stringValue3=stringValue4\",rtp=1700");
+ "bs,rtp=1700");
}
@Test
- public void createInstance_populatesCmcdHttpQueryParameters() {
+ public void createInstance_audioObjectType_setsCorrectQueryParameters() {
CmcdConfiguration.Factory cmcdConfigurationFactory =
mediaItem ->
new CmcdConfiguration(
"sessionId",
mediaItem.mediaId,
new CmcdConfiguration.RequestConfig() {
- @Override
- public ImmutableListMultimap<@CmcdConfiguration.HeaderKey String, String>
- getCustomData() {
- return new ImmutableListMultimap.Builder()
- .put("CMCD-Object", "key-1=1")
- .put("CMCD-Request", "key-2=\"stringVälue1,stringVälue2\"")
- .build();
- }
-
@Override
public int getRequestedMaximumThroughputKbps(int throughputKbps) {
return 2 * throughputKbps;
@@ -129,32 +144,148 @@ public class CmcdDataTest {
when(trackSelection.getLatestBitrateEstimate()).thenReturn(500_000L);
DataSpec dataSpec = new DataSpec.Builder().setUri(Uri.EMPTY).build();
CmcdData cmcdData =
- new CmcdData.Factory(
- cmcdConfiguration,
- trackSelection,
- /* bufferedDurationUs= */ 1_760_000,
- /* playbackRate= */ 2.0f,
- /* streamingFormat= */ CmcdData.Factory.STREAMING_FORMAT_DASH,
- /* isLive= */ true,
- /* didRebuffer= */ true,
- /* isBufferEmpty= */ false)
+ new CmcdData.Factory(cmcdConfiguration, CmcdData.Factory.STREAMING_FORMAT_DASH)
+ .setObjectType(CmcdData.Factory.OBJECT_TYPE_AUDIO_ONLY)
+ .setTrackSelection(trackSelection)
+ .setBufferedDurationUs(1_760_000)
+ .setPlaybackRate(2.0f)
+ .setIsLive(true)
+ .setDidRebuffer(true)
+ .setIsBufferEmpty(false)
.setChunkDurationUs(3_000_000)
.createCmcdData();
dataSpec = cmcdData.addToDataSpec(dataSpec);
- // Confirm that the values above are URL-encoded
- assertThat(dataSpec.uri.toString()).doesNotContain("ä");
- assertThat(dataSpec.uri.toString()).contains(Uri.encode("ä"));
assertThat(dataSpec.uri.getQueryParameter(CmcdConfiguration.CMCD_QUERY_PARAMETER_KEY))
.isEqualTo(
- "bl=1800,br=840,bs,cid=\"mediaId\",d=3000,dl=900,key-1=1,"
- + "key-2=\"stringVälue1,stringVälue2\",mtp=500,pr=2.00,rtp=1700,sf=d,"
- + "sid=\"sessionId\",st=l,su,tb=1000");
+ "bl=1800,br=840,bs,cid=\"mediaId\",d=3000,dl=900,mtp=500,ot=a,pr=2.00,"
+ + "rtp=1700,sf=d,sid=\"sessionId\",st=l,su,tb=1000");
}
@Test
- public void createInstance_withInvalidNonHyphenatedCustomKey_throwsIllegalStateException() {
+ public void createInstance_manifestObjectType_setsCorrectHttpHeaders() {
+ CmcdConfiguration.Factory cmcdConfigurationFactory =
+ mediaItem ->
+ new CmcdConfiguration(
+ "sessionId", mediaItem.mediaId, new CmcdConfiguration.RequestConfig() {});
+ MediaItem mediaItem = new MediaItem.Builder().setMediaId("mediaId").build();
+ CmcdConfiguration cmcdConfiguration =
+ cmcdConfigurationFactory.createCmcdConfiguration(mediaItem);
+ DataSpec dataSpec = new DataSpec.Builder().setUri(Uri.EMPTY).build();
+ CmcdData cmcdData =
+ new CmcdData.Factory(cmcdConfiguration, CmcdData.Factory.STREAMING_FORMAT_DASH)
+ .setObjectType(CmcdData.Factory.OBJECT_TYPE_MANIFEST)
+ .createCmcdData();
+
+ dataSpec = cmcdData.addToDataSpec(dataSpec);
+
+ assertThat(dataSpec.httpRequestHeaders)
+ .containsExactly(
+ "CMCD-Object", "ot=m", "CMCD-Session", "cid=\"mediaId\",sf=d,sid=\"sessionId\"");
+ }
+
+ @Test
+ public void createInstance_manifestObjectType_setsCorrectQueryParameters() {
+ CmcdConfiguration.Factory cmcdConfigurationFactory =
+ mediaItem ->
+ new CmcdConfiguration(
+ "sessionId",
+ mediaItem.mediaId,
+ new CmcdConfiguration.RequestConfig() {},
+ CmcdConfiguration.MODE_QUERY_PARAMETER);
+ MediaItem mediaItem = new MediaItem.Builder().setMediaId("mediaId").build();
+ CmcdConfiguration cmcdConfiguration =
+ cmcdConfigurationFactory.createCmcdConfiguration(mediaItem);
+ DataSpec dataSpec = new DataSpec.Builder().setUri(Uri.EMPTY).build();
+ CmcdData cmcdData =
+ new CmcdData.Factory(cmcdConfiguration, CmcdData.Factory.STREAMING_FORMAT_DASH)
+ .setObjectType(CmcdData.Factory.OBJECT_TYPE_MANIFEST)
+ .createCmcdData();
+
+ dataSpec = cmcdData.addToDataSpec(dataSpec);
+
+ assertThat(dataSpec.uri.getQueryParameter(CmcdConfiguration.CMCD_QUERY_PARAMETER_KEY))
+ .isEqualTo("cid=\"mediaId\",ot=m,sf=d,sid=\"sessionId\"");
+ }
+
+ @Test
+ public void createInstance_unsetObjectType_setsCorrectHttpHeaders() {
+ CmcdConfiguration.Factory cmcdConfigurationFactory =
+ mediaItem ->
+ new CmcdConfiguration(
+ "sessionId", mediaItem.mediaId, new CmcdConfiguration.RequestConfig() {});
+ MediaItem mediaItem = new MediaItem.Builder().setMediaId("mediaId").build();
+ CmcdConfiguration cmcdConfiguration =
+ cmcdConfigurationFactory.createCmcdConfiguration(mediaItem);
+ ExoTrackSelection trackSelection = mock(ExoTrackSelection.class);
+ Format format = new Format.Builder().setPeakBitrate(840_000).build();
+ when(trackSelection.getSelectedFormat()).thenReturn(format);
+ when(trackSelection.getTrackGroup())
+ .thenReturn(new TrackGroup(format, new Format.Builder().setPeakBitrate(1_000_000).build()));
+ when(trackSelection.getLatestBitrateEstimate()).thenReturn(500_000L);
+ DataSpec dataSpec = new DataSpec.Builder().setUri(Uri.EMPTY).build();
+ CmcdData cmcdData =
+ new CmcdData.Factory(cmcdConfiguration, CmcdData.Factory.STREAMING_FORMAT_DASH)
+ .setTrackSelection(trackSelection)
+ .setPlaybackRate(2.0f)
+ .setIsLive(true)
+ .setDidRebuffer(true)
+ .setIsBufferEmpty(false)
+ .createCmcdData();
+
+ dataSpec = cmcdData.addToDataSpec(dataSpec);
+
+ assertThat(dataSpec.httpRequestHeaders)
+ .containsExactly(
+ "CMCD-Object",
+ "br=840,tb=1000",
+ "CMCD-Request",
+ "mtp=500,su",
+ "CMCD-Session",
+ "cid=\"mediaId\",pr=2.00,sf=d,sid=\"sessionId\",st=l",
+ "CMCD-Status",
+ "bs");
+ }
+
+ @Test
+ public void createInstance_unsetObjectType_setsCorrectQueryParameters() {
+ CmcdConfiguration.Factory cmcdConfigurationFactory =
+ mediaItem ->
+ new CmcdConfiguration(
+ "sessionId",
+ mediaItem.mediaId,
+ new CmcdConfiguration.RequestConfig() {},
+ CmcdConfiguration.MODE_QUERY_PARAMETER);
+ MediaItem mediaItem = new MediaItem.Builder().setMediaId("mediaId").build();
+ CmcdConfiguration cmcdConfiguration =
+ cmcdConfigurationFactory.createCmcdConfiguration(mediaItem);
+ ExoTrackSelection trackSelection = mock(ExoTrackSelection.class);
+ Format format = new Format.Builder().setPeakBitrate(840_000).build();
+ when(trackSelection.getSelectedFormat()).thenReturn(format);
+ when(trackSelection.getTrackGroup())
+ .thenReturn(new TrackGroup(format, new Format.Builder().setPeakBitrate(1_000_000).build()));
+ when(trackSelection.getLatestBitrateEstimate()).thenReturn(500_000L);
+ DataSpec dataSpec = new DataSpec.Builder().setUri(Uri.EMPTY).build();
+ CmcdData cmcdData =
+ new CmcdData.Factory(cmcdConfiguration, CmcdData.Factory.STREAMING_FORMAT_DASH)
+ .setTrackSelection(trackSelection)
+ .setPlaybackRate(2.0f)
+ .setIsLive(true)
+ .setDidRebuffer(true)
+ .setIsBufferEmpty(false)
+ .createCmcdData();
+
+ dataSpec = cmcdData.addToDataSpec(dataSpec);
+
+ assertThat(dataSpec.uri.getQueryParameter(CmcdConfiguration.CMCD_QUERY_PARAMETER_KEY))
+ .isEqualTo(
+ "br=840,bs,cid=\"mediaId\",mtp=500,pr=2.00,"
+ + "sf=d,sid=\"sessionId\",st=l,su,tb=1000");
+ }
+
+ @Test
+ public void createInstance_customData_setsCorrectHttpHeaders() {
CmcdConfiguration.Factory cmcdConfigurationFactory =
mediaItem ->
new CmcdConfiguration(
@@ -164,27 +295,94 @@ public class CmcdDataTest {
@Override
public ImmutableListMultimap<@CmcdConfiguration.HeaderKey String, String>
getCustomData() {
+ return new ImmutableListMultimap.Builder()
+ .putAll("CMCD-Object", "key-1=1", "key-2-separated-by-multiple-hyphens=2")
+ .put("CMCD-Request", "key-3=\"stringValue1,stringValue2\"")
+ .put("CMCD-Session", "key-4=0.5")
+ .put("CMCD-Status", "key-5=\"stringValue3=stringValue4\"")
+ .build();
+ }
+ });
+ CmcdConfiguration cmcdConfiguration =
+ cmcdConfigurationFactory.createCmcdConfiguration(MediaItem.EMPTY);
+ DataSpec dataSpec = new DataSpec.Builder().setUri(Uri.EMPTY).build();
+ CmcdData cmcdData =
+ new CmcdData.Factory(cmcdConfiguration, CmcdData.Factory.STREAMING_FORMAT_DASH)
+ .setObjectType(CmcdData.Factory.OBJECT_TYPE_MANIFEST)
+ .createCmcdData();
+
+ dataSpec = cmcdData.addToDataSpec(dataSpec);
+
+ assertThat(dataSpec.httpRequestHeaders)
+ .containsExactly(
+ "CMCD-Object",
+ "key-1=1,key-2-separated-by-multiple-hyphens=2,ot=m",
+ "CMCD-Request",
+ "key-3=\"stringValue1,stringValue2\"",
+ "CMCD-Session",
+ "key-4=0.5,sf=d",
+ "CMCD-Status",
+ "key-5=\"stringValue3=stringValue4\"");
+ }
+
+ @Test
+ public void createInstance_customData_setsCorrectQueryParameters() {
+ CmcdConfiguration.Factory cmcdConfigurationFactory =
+ mediaItem ->
+ new CmcdConfiguration(
+ null,
+ null,
+ new CmcdConfiguration.RequestConfig() {
+ @Override
+ public ImmutableListMultimap<@CmcdConfiguration.HeaderKey String, String>
+ getCustomData() {
+ return new ImmutableListMultimap.Builder()
+ .put("CMCD-Object", "key-1=1")
+ .put("CMCD-Request", "key-2=\"stringVälue1,stringVälue2\"")
+ .build();
+ }
+ },
+ CmcdConfiguration.MODE_QUERY_PARAMETER);
+ CmcdConfiguration cmcdConfiguration =
+ cmcdConfigurationFactory.createCmcdConfiguration(MediaItem.EMPTY);
+ DataSpec dataSpec = new DataSpec.Builder().setUri(Uri.EMPTY).build();
+ CmcdData cmcdData =
+ new CmcdData.Factory(cmcdConfiguration, CmcdData.Factory.STREAMING_FORMAT_DASH)
+ .setObjectType(CmcdData.Factory.OBJECT_TYPE_MANIFEST)
+ .createCmcdData();
+
+ dataSpec = cmcdData.addToDataSpec(dataSpec);
+
+ // Confirm that the values above are URL-encoded
+ assertThat(dataSpec.uri.toString()).doesNotContain("ä");
+ assertThat(dataSpec.uri.toString()).contains(Uri.encode("ä"));
+ assertThat(dataSpec.uri.getQueryParameter(CmcdConfiguration.CMCD_QUERY_PARAMETER_KEY))
+ .isEqualTo("key-1=1,key-2=\"stringVälue1,stringVälue2\",ot=m,sf=d");
+ }
+
+ @Test
+ public void createInstance_invalidCustomDataKey_throwsException() {
+ CmcdConfiguration.Factory cmcdConfigurationFactory =
+ mediaItem ->
+ new CmcdConfiguration(
+ null,
+ null,
+ new CmcdConfiguration.RequestConfig() {
+ @Override
+ public ImmutableListMultimap<@CmcdConfiguration.HeaderKey String, String>
+ getCustomData() {
+ // Invalid non-hyphenated key1
return ImmutableListMultimap.of("CMCD-Object", "key1=1");
}
});
- MediaItem mediaItem = new MediaItem.Builder().setMediaId("mediaId").build();
CmcdConfiguration cmcdConfiguration =
- cmcdConfigurationFactory.createCmcdConfiguration(mediaItem);
- ExoTrackSelection trackSelection = mock(ExoTrackSelection.class);
- when(trackSelection.getSelectedFormat()).thenReturn(new Format.Builder().build());
+ cmcdConfigurationFactory.createCmcdConfiguration(MediaItem.EMPTY);
assertThrows(
IllegalStateException.class,
() ->
- new CmcdData.Factory(
- cmcdConfiguration,
- trackSelection,
- /* bufferedDurationUs= */ 0,
- /* playbackRate= */ 1.0f,
- /* streamingFormat= */ CmcdData.Factory.STREAMING_FORMAT_DASH,
- /* isLive= */ true,
- /* didRebuffer= */ true,
- /* isBufferEmpty= */ false)
+ new CmcdData.Factory(cmcdConfiguration, CmcdData.Factory.STREAMING_FORMAT_DASH)
+ .setObjectType(CmcdData.Factory.OBJECT_TYPE_MANIFEST)
.createCmcdData());
}
}
diff --git a/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DashMediaSource.java b/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DashMediaSource.java
index 0ef7df98db..d2d67c80c5 100644
--- a/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DashMediaSource.java
+++ b/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DashMediaSource.java
@@ -43,6 +43,7 @@ import androidx.media3.common.util.Log;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.datasource.DataSource;
+import androidx.media3.datasource.DataSpec;
import androidx.media3.datasource.TransferListener;
import androidx.media3.exoplayer.dash.PlayerEmsgHandler.PlayerEmsgCallback;
import androidx.media3.exoplayer.dash.manifest.AdaptationSet;
@@ -69,6 +70,7 @@ import androidx.media3.exoplayer.source.MediaSourceFactory;
import androidx.media3.exoplayer.source.SequenceableLoader;
import androidx.media3.exoplayer.upstream.Allocator;
import androidx.media3.exoplayer.upstream.CmcdConfiguration;
+import androidx.media3.exoplayer.upstream.CmcdData;
import androidx.media3.exoplayer.upstream.DefaultLoadErrorHandlingPolicy;
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy;
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy.LoadErrorInfo;
@@ -1099,8 +1101,19 @@ public final class DashMediaSource extends BaseMediaSource {
manifestUri = this.manifestUri;
}
manifestLoadPending = false;
+ DataSpec dataSpec =
+ new DataSpec.Builder().setUri(manifestUri).setFlags(DataSpec.FLAG_ALLOW_GZIP).build();
+ if (cmcdConfiguration != null) {
+ CmcdData.Factory cmcdDataFactory =
+ new CmcdData.Factory(cmcdConfiguration, CmcdData.Factory.STREAMING_FORMAT_DASH)
+ .setObjectType(CmcdData.Factory.OBJECT_TYPE_MANIFEST);
+ if (manifest != null) {
+ cmcdDataFactory.setIsLive(manifest.dynamic);
+ }
+ cmcdDataFactory.createCmcdData().addToDataSpec(dataSpec);
+ }
startLoading(
- new ParsingLoadable<>(dataSource, manifestUri, C.DATA_TYPE_MANIFEST, manifestParser),
+ new ParsingLoadable<>(dataSource, dataSpec, C.DATA_TYPE_MANIFEST, manifestParser),
manifestCallback,
loadErrorHandlingPolicy.getMinimumLoadableRetryCount(C.DATA_TYPE_MANIFEST));
}
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 e94de25ea3..b651ce73fe 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
@@ -421,15 +421,13 @@ public class DefaultDashChunkSource implements DashChunkSource {
CmcdData.Factory cmcdDataFactory =
cmcdConfiguration == null
? null
- : new CmcdData.Factory(
- cmcdConfiguration,
- trackSelection,
- max(0, bufferedDurationUs),
- /* playbackRate= */ loadingInfo.playbackSpeed,
- /* streamingFormat= */ CmcdData.Factory.STREAMING_FORMAT_DASH,
- /* isLive= */ manifest.dynamic,
- /* didRebuffer= */ loadingInfo.rebufferedSince(lastChunkRequestRealtimeMs),
- /* isBufferEmpty= */ queue.isEmpty());
+ : new CmcdData.Factory(cmcdConfiguration, CmcdData.Factory.STREAMING_FORMAT_DASH)
+ .setTrackSelection(trackSelection)
+ .setBufferedDurationUs(max(0, bufferedDurationUs))
+ .setPlaybackRate(loadingInfo.playbackSpeed)
+ .setIsLive(manifest.dynamic)
+ .setDidRebuffer(loadingInfo.rebufferedSince(lastChunkRequestRealtimeMs))
+ .setIsBufferEmpty(queue.isEmpty());
lastChunkRequestRealtimeMs = SystemClock.elapsedRealtime();
RepresentationHolder representationHolder = updateSelectedBaseUrl(selectedTrackIndex);
@@ -715,7 +713,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
* indexUri} is not {@code null}.
* @param indexUri The URI pointing to index data. Can be {@code null} if {@code
* initializationUri} is not {@code null}.
- * @param cmcdDataFactory The {@link CmcdData.Factory} for generating CMCD data.
+ * @param cmcdDataFactory The {@link CmcdData.Factory} for generating {@link CmcdData}.
*/
@RequiresNonNull("#1.chunkExtractor")
protected Chunk newInitializationChunk(
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 6867a3291e..68a5374fb9 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
@@ -507,20 +507,17 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@Nullable CmcdData.Factory cmcdDataFactory = null;
if (cmcdConfiguration != null) {
cmcdDataFactory =
- new CmcdData.Factory(
- cmcdConfiguration,
- trackSelection,
- max(0, bufferedDurationUs),
- /* playbackRate= */ loadingInfo.playbackSpeed,
- /* streamingFormat= */ CmcdData.Factory.STREAMING_FORMAT_HLS,
- /* isLive= */ !playlist.hasEndTag,
- /* didRebuffer= */ loadingInfo.rebufferedSince(lastChunkRequestRealtimeMs),
- /* isBufferEmpty= */ queue.isEmpty())
+ new CmcdData.Factory(cmcdConfiguration, CmcdData.Factory.STREAMING_FORMAT_HLS)
+ .setTrackSelection(trackSelection)
+ .setBufferedDurationUs(max(0, bufferedDurationUs))
+ .setPlaybackRate(loadingInfo.playbackSpeed)
+ .setIsLive(!playlist.hasEndTag)
+ .setDidRebuffer(loadingInfo.rebufferedSince(lastChunkRequestRealtimeMs))
+ .setIsBufferEmpty(queue.isEmpty())
.setObjectType(
getIsMuxedAudioAndVideo()
? CmcdData.Factory.OBJECT_TYPE_MUXED_AUDIO_AND_VIDEO
: CmcdData.Factory.getObjectType(trackSelection));
-
long nextMediaSequence =
segmentBaseHolder.partIndex == C.INDEX_UNSET
? segmentBaseHolder.mediaSequence + 1
diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaSource.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaSource.java
index 7985d7211a..df972d81a6 100644
--- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaSource.java
+++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaSource.java
@@ -417,7 +417,10 @@ public final class HlsMediaSource extends BaseMediaSource
drmSessionManagerProvider.get(mediaItem),
loadErrorHandlingPolicy,
playlistTrackerFactory.createTracker(
- hlsDataSourceFactory, loadErrorHandlingPolicy, playlistParserFactory),
+ hlsDataSourceFactory,
+ loadErrorHandlingPolicy,
+ playlistParserFactory,
+ cmcdConfiguration),
elapsedRealTimeOffsetMs,
allowChunklessPreparation,
metadataType,
diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/DefaultHlsPlaylistTracker.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/DefaultHlsPlaylistTracker.java
index 505841af42..9254177423 100644
--- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/DefaultHlsPlaylistTracker.java
+++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/DefaultHlsPlaylistTracker.java
@@ -29,6 +29,7 @@ import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.datasource.DataSource;
+import androidx.media3.datasource.DataSpec;
import androidx.media3.datasource.HttpDataSource;
import androidx.media3.exoplayer.hls.HlsDataSourceFactory;
import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist.Part;
@@ -38,6 +39,8 @@ import androidx.media3.exoplayer.hls.playlist.HlsMultivariantPlaylist.Variant;
import androidx.media3.exoplayer.source.LoadEventInfo;
import androidx.media3.exoplayer.source.MediaLoadData;
import androidx.media3.exoplayer.source.MediaSourceEventListener.EventDispatcher;
+import androidx.media3.exoplayer.upstream.CmcdConfiguration;
+import androidx.media3.exoplayer.upstream.CmcdData;
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy;
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy.LoadErrorInfo;
import androidx.media3.exoplayer.upstream.Loader;
@@ -69,6 +72,7 @@ public final class DefaultHlsPlaylistTracker
private final HashMap playlistBundles;
private final CopyOnWriteArrayList listeners;
private final double playlistStuckTargetDurationCoefficient;
+ @Nullable private final CmcdConfiguration cmcdConfiguration;
@Nullable private EventDispatcher eventDispatcher;
@Nullable private Loader initialPlaylistLoader;
@@ -86,15 +90,18 @@ public final class DefaultHlsPlaylistTracker
* @param dataSourceFactory A factory for {@link DataSource} instances.
* @param loadErrorHandlingPolicy The {@link LoadErrorHandlingPolicy}.
* @param playlistParserFactory An {@link HlsPlaylistParserFactory}.
+ * @param cmcdConfiguration The {@link CmcdConfiguration}.
*/
public DefaultHlsPlaylistTracker(
HlsDataSourceFactory dataSourceFactory,
LoadErrorHandlingPolicy loadErrorHandlingPolicy,
- HlsPlaylistParserFactory playlistParserFactory) {
+ HlsPlaylistParserFactory playlistParserFactory,
+ @Nullable CmcdConfiguration cmcdConfiguration) {
this(
dataSourceFactory,
loadErrorHandlingPolicy,
playlistParserFactory,
+ cmcdConfiguration,
DEFAULT_PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT);
}
@@ -104,6 +111,7 @@ public final class DefaultHlsPlaylistTracker
* @param dataSourceFactory A factory for {@link DataSource} instances.
* @param loadErrorHandlingPolicy The {@link LoadErrorHandlingPolicy}.
* @param playlistParserFactory An {@link HlsPlaylistParserFactory}.
+ * @param cmcdConfiguration The {@link CmcdConfiguration}.
* @param playlistStuckTargetDurationCoefficient A coefficient to apply to the target duration of
* media playlists in order to determine that a non-changing playlist is stuck. Once a
* playlist is deemed stuck, a {@link PlaylistStuckException} is thrown via {@link
@@ -113,10 +121,12 @@ public final class DefaultHlsPlaylistTracker
HlsDataSourceFactory dataSourceFactory,
LoadErrorHandlingPolicy loadErrorHandlingPolicy,
HlsPlaylistParserFactory playlistParserFactory,
+ @Nullable CmcdConfiguration cmcdConfiguration,
double playlistStuckTargetDurationCoefficient) {
this.dataSourceFactory = dataSourceFactory;
this.playlistParserFactory = playlistParserFactory;
this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
+ this.cmcdConfiguration = cmcdConfiguration;
this.playlistStuckTargetDurationCoefficient = playlistStuckTargetDurationCoefficient;
listeners = new CopyOnWriteArrayList<>();
playlistBundles = new HashMap<>();
@@ -133,10 +143,22 @@ public final class DefaultHlsPlaylistTracker
this.playlistRefreshHandler = Util.createHandlerForCurrentLooper();
this.eventDispatcher = eventDispatcher;
this.primaryPlaylistListener = primaryPlaylistListener;
+ DataSpec dataSpec =
+ new DataSpec.Builder()
+ .setUri(initialPlaylistUri)
+ .setFlags(DataSpec.FLAG_ALLOW_GZIP)
+ .build();
+ if (cmcdConfiguration != null) {
+ CmcdData cmcdData =
+ new CmcdData.Factory(cmcdConfiguration, CmcdData.Factory.STREAMING_FORMAT_HLS)
+ .setObjectType(CmcdData.Factory.OBJECT_TYPE_MANIFEST)
+ .createCmcdData();
+ cmcdData.addToDataSpec(dataSpec);
+ }
ParsingLoadable multivariantPlaylistLoadable =
new ParsingLoadable<>(
dataSourceFactory.createDataSource(C.DATA_TYPE_MANIFEST),
- initialPlaylistUri,
+ dataSpec,
C.DATA_TYPE_MANIFEST,
playlistParserFactory.createPlaylistParser());
Assertions.checkState(initialPlaylistLoader == null);
@@ -762,12 +784,23 @@ public final class DefaultHlsPlaylistTracker
private void loadPlaylistImmediately(Uri playlistRequestUri) {
ParsingLoadable.Parser mediaPlaylistParser =
playlistParserFactory.createPlaylistParser(multivariantPlaylist, playlistSnapshot);
+ DataSpec dataSpec =
+ new DataSpec.Builder()
+ .setUri(playlistRequestUri)
+ .setFlags(DataSpec.FLAG_ALLOW_GZIP)
+ .build();
+ if (cmcdConfiguration != null) {
+ CmcdData.Factory cmcdDataFactory =
+ new CmcdData.Factory(cmcdConfiguration, CmcdData.Factory.STREAMING_FORMAT_HLS)
+ .setObjectType(CmcdData.Factory.OBJECT_TYPE_MANIFEST);
+ if (primaryMediaPlaylistSnapshot != null) {
+ cmcdDataFactory.setIsLive(!primaryMediaPlaylistSnapshot.hasEndTag);
+ }
+ cmcdDataFactory.createCmcdData().addToDataSpec(dataSpec);
+ }
ParsingLoadable mediaPlaylistLoadable =
new ParsingLoadable<>(
- mediaPlaylistDataSource,
- playlistRequestUri,
- C.DATA_TYPE_MANIFEST,
- mediaPlaylistParser);
+ mediaPlaylistDataSource, dataSpec, C.DATA_TYPE_MANIFEST, mediaPlaylistParser);
mediaPlaylistLoader.startLoading(
mediaPlaylistLoadable,
/* callback= */ this,
diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsPlaylistTracker.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsPlaylistTracker.java
index d08874232a..b42128bae0 100644
--- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsPlaylistTracker.java
+++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsPlaylistTracker.java
@@ -21,6 +21,7 @@ import androidx.media3.common.C;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.exoplayer.hls.HlsDataSourceFactory;
import androidx.media3.exoplayer.source.MediaSourceEventListener.EventDispatcher;
+import androidx.media3.exoplayer.upstream.CmcdConfiguration;
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy;
import java.io.IOException;
@@ -48,11 +49,13 @@ public interface HlsPlaylistTracker {
* @param dataSourceFactory The {@link HlsDataSourceFactory} to use for playlist loading.
* @param loadErrorHandlingPolicy The {@link LoadErrorHandlingPolicy} for playlist load errors.
* @param playlistParserFactory The {@link HlsPlaylistParserFactory} for playlist parsing.
+ * @param cmcdConfiguration The {@link CmcdConfiguration} to use for playlist loading.
*/
HlsPlaylistTracker createTracker(
HlsDataSourceFactory dataSourceFactory,
LoadErrorHandlingPolicy loadErrorHandlingPolicy,
- HlsPlaylistParserFactory playlistParserFactory);
+ HlsPlaylistParserFactory playlistParserFactory,
+ @Nullable CmcdConfiguration cmcdConfiguration);
}
/** Listener for primary playlist changes. */
diff --git a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/playlist/DefaultHlsPlaylistTrackerTest.java b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/playlist/DefaultHlsPlaylistTrackerTest.java
index 23000b89c7..67420ec0aa 100644
--- a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/playlist/DefaultHlsPlaylistTrackerTest.java
+++ b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/playlist/DefaultHlsPlaylistTrackerTest.java
@@ -404,7 +404,8 @@ public class DefaultHlsPlaylistTrackerTest {
new DefaultHlsPlaylistTracker(
dataType -> new DefaultHttpDataSource.Factory().createDataSource(),
new DefaultLoadErrorHandlingPolicy(),
- new DefaultHlsPlaylistParserFactory());
+ new DefaultHlsPlaylistParserFactory(),
+ /* cmcdConfiguration= */ null);
AtomicInteger playlistChangedCounter = new AtomicInteger();
AtomicReference audioPlaylistRefreshExceptionRef = new AtomicReference<>();
defaultHlsPlaylistTracker.addListener(
@@ -486,7 +487,8 @@ public class DefaultHlsPlaylistTrackerTest {
new DefaultHlsPlaylistTracker(
dataType -> new DefaultHttpDataSource.Factory().createDataSource(),
new DefaultLoadErrorHandlingPolicy(),
- new DefaultHlsPlaylistParserFactory());
+ new DefaultHlsPlaylistParserFactory(),
+ /* cmcdConfiguration= */ null);
List mediaPlaylists = new ArrayList<>();
AtomicInteger playlistCounter = new AtomicInteger();
AtomicReference primaryPlaylistChangeExceptionRef = new AtomicReference<>();
@@ -562,7 +564,8 @@ public class DefaultHlsPlaylistTrackerTest {
new DefaultHlsPlaylistTracker(
dataType -> new DefaultHttpDataSource.Factory().createDataSource(),
new DefaultLoadErrorHandlingPolicy(),
- new DefaultHlsPlaylistParserFactory());
+ new DefaultHlsPlaylistParserFactory(),
+ /* cmcdConfiguration= */ null);
List mediaPlaylists = new ArrayList<>();
AtomicInteger playlistCounter = new AtomicInteger();
AtomicReference playlistRefreshExceptionRef = new AtomicReference<>();
@@ -674,7 +677,8 @@ public class DefaultHlsPlaylistTrackerTest {
new DefaultHlsPlaylistTracker(
dataType -> dataSourceFactory.createDataSource(),
new DefaultLoadErrorHandlingPolicy(),
- new DefaultHlsPlaylistParserFactory());
+ new DefaultHlsPlaylistParserFactory(),
+ /* cmcdConfiguration= */ null);
List mediaPlaylists = new ArrayList<>();
AtomicInteger playlistCounter = new AtomicInteger();
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 311afb3c7b..3d0b9d51ca 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
@@ -360,15 +360,13 @@ public class DefaultSsChunkSource implements SsChunkSource {
@Nullable CmcdData.Factory cmcdDataFactory = null;
if (cmcdConfiguration != null) {
cmcdDataFactory =
- new CmcdData.Factory(
- cmcdConfiguration,
- trackSelection,
- max(0, bufferedDurationUs),
- /* playbackRate= */ loadingInfo.playbackSpeed,
- /* streamingFormat= */ CmcdData.Factory.STREAMING_FORMAT_SS,
- /* isLive= */ manifest.isLive,
- /* didRebuffer= */ loadingInfo.rebufferedSince(lastChunkRequestRealtimeMs),
- /* isBufferEmpty= */ queue.isEmpty())
+ new CmcdData.Factory(cmcdConfiguration, CmcdData.Factory.STREAMING_FORMAT_SS)
+ .setTrackSelection(trackSelection)
+ .setBufferedDurationUs(max(0, bufferedDurationUs))
+ .setPlaybackRate(loadingInfo.playbackSpeed)
+ .setIsLive(manifest.isLive)
+ .setDidRebuffer(loadingInfo.rebufferedSince(lastChunkRequestRealtimeMs))
+ .setIsBufferEmpty(queue.isEmpty())
.setChunkDurationUs(chunkEndTimeUs - chunkStartTimeUs)
.setObjectType(CmcdData.Factory.getObjectType(trackSelection));
diff --git a/libraries/exoplayer_smoothstreaming/src/main/java/androidx/media3/exoplayer/smoothstreaming/SsMediaSource.java b/libraries/exoplayer_smoothstreaming/src/main/java/androidx/media3/exoplayer/smoothstreaming/SsMediaSource.java
index e402753125..a2dfd4b783 100644
--- a/libraries/exoplayer_smoothstreaming/src/main/java/androidx/media3/exoplayer/smoothstreaming/SsMediaSource.java
+++ b/libraries/exoplayer_smoothstreaming/src/main/java/androidx/media3/exoplayer/smoothstreaming/SsMediaSource.java
@@ -35,6 +35,7 @@ import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.datasource.DataSource;
+import androidx.media3.datasource.DataSpec;
import androidx.media3.datasource.TransferListener;
import androidx.media3.exoplayer.drm.DefaultDrmSessionManagerProvider;
import androidx.media3.exoplayer.drm.DrmSessionEventListener;
@@ -58,6 +59,7 @@ import androidx.media3.exoplayer.source.SequenceableLoader;
import androidx.media3.exoplayer.source.SinglePeriodTimeline;
import androidx.media3.exoplayer.upstream.Allocator;
import androidx.media3.exoplayer.upstream.CmcdConfiguration;
+import androidx.media3.exoplayer.upstream.CmcdData;
import androidx.media3.exoplayer.upstream.DefaultLoadErrorHandlingPolicy;
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy;
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy.LoadErrorInfo;
@@ -676,9 +678,19 @@ public final class SsMediaSource extends BaseMediaSource
if (manifestLoader.hasFatalError()) {
return;
}
+ DataSpec dataSpec =
+ new DataSpec.Builder().setUri(manifestUri).setFlags(DataSpec.FLAG_ALLOW_GZIP).build();
+ if (cmcdConfiguration != null) {
+ CmcdData.Factory cmcdDataFactory =
+ new CmcdData.Factory(cmcdConfiguration, CmcdData.Factory.STREAMING_FORMAT_SS)
+ .setObjectType(CmcdData.Factory.OBJECT_TYPE_MANIFEST);
+ if (manifest != null) {
+ cmcdDataFactory.setIsLive(manifest.isLive);
+ }
+ cmcdDataFactory.createCmcdData().addToDataSpec(dataSpec);
+ }
ParsingLoadable loadable =
- new ParsingLoadable<>(
- manifestDataSource, manifestUri, C.DATA_TYPE_MANIFEST, manifestParser);
+ new ParsingLoadable<>(manifestDataSource, dataSpec, C.DATA_TYPE_MANIFEST, manifestParser);
manifestLoader.startLoading(
loadable, this, loadErrorHandlingPolicy.getMinimumLoadableRetryCount(loadable.type));
}