Make custom data adhere to Common Media Client Data(CMCD) specification

Added more comprehensive Javadoc around setting custom data and verification on key format.

Changed adding custom data as a `String` to `List<String>`. This would enable us to sort all keys to reduce the fingerprinting surface.

PiperOrigin-RevId: 558291240
This commit is contained in:
rohks 2023-08-19 01:40:38 +01:00 committed by Julia Bibik
parent 6566387f70
commit afb8d6c9e2
7 changed files with 204 additions and 135 deletions

View File

@ -24,7 +24,7 @@ import androidx.annotation.StringDef;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.common.util.UnstableApi;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableListMultimap;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@ -160,24 +160,39 @@ public final class CmcdConfiguration {
*
* <p>By default, no custom data is provided.
*
* <p>The key should belong to the {@link HeaderKey}. The value should consist of key-value
* pairs separated by commas. If the value contains one of the keys defined in the {@link
* CmcdKey} list, then this key should not be {@linkplain #isKeyAllowed(String) allowed},
* otherwise the key could be included twice in the produced log.
* <p>The data payload consists of a series of key/value pairs constructed according to the
* following rules:
*
* <ul>
* <li>Custom keys SHOULD be allocated to one of the four defined header names defined in the
* {@link HeaderKey} annotation.
* <li>All information in the payload MUST be represented as key=value pairs.
* <li>The key and value MUST be separated by an equals sign. If the value type is boolean and
* the value is {@code true}, then the equals sign and the value MUST be omitted.
* <li>The key names are case-sensitive and reserved. Custom key names MUST carry a hyphenated
* prefix to ensure no namespace collision with future revisions to Common Media Client
* Data (CMCD) specification. Clients SHOULD use a reverse-DNS syntax when defining their
* own prefix.
* <li>Any value of type String MUST be enclosed by opening and closing double quotes. Double
* quotes and backslashes MUST be escaped using a backslash "\" character. Any value that
* is not of type string does not require quoting.
* </ul>
*
* <p><b>Note:</b> The key words MUST and SHOULD are to be interpreted as described in RFC 2119.
*
* <p>Example:
*
* <ul>
* <li>CMCD-Request:customField1=25400
* <li>CMCD-Object:customField2=3200,customField3=4004,customField4=v,customField5=6000
* <li>CMCD-Status:customField6,customField7=15000
* <li>CMCD-Session:customField8="6e2fb550-c457-11e9-bb97-0800200c9a66"
* <li>CMCD-Request:custom-field1=25400
* <li>CMCD-Object:custom-field2=3200,custom-field3=4004,custom-field4=v,custom-field5=6000
* <li>CMCD-Status:custom-field6,custom-field7=15000
* <li>CMCD-Session:custom-field8="stringValue"
* </ul>
*
* @return An {@link ImmutableMap} containing the custom data.
* @return An {@link ImmutableListMultimap} containing the custom data.
*/
default ImmutableMap<@HeaderKey String, String> getCustomData() {
return ImmutableMap.of();
default ImmutableListMultimap<@HeaderKey String, String> getCustomData() {
return ImmutableListMultimap.of();
}
/**

View File

@ -16,6 +16,7 @@
package androidx.media3.exoplayer.upstream;
import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkState;
import static java.lang.Math.max;
import static java.lang.annotation.ElementType.TYPE_USE;
@ -30,6 +31,8 @@ import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.lang.annotation.Documented;
@ -37,6 +40,8 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
/**
* This class serves as a factory for generating Common Media Client Data (CMCD) HTTP request
@ -50,6 +55,8 @@ import java.util.ArrayList;
public final class CmcdHeadersFactory {
private static final Joiner COMMA_JOINER = Joiner.on(",");
private static final Pattern CUSTOM_KEY_NAME_PATTERN =
Pattern.compile("[a-zA-Z0-9]+(-[a-zA-Z0-9]+)+");
/**
* Retrieves the object type value from the given {@link ExoTrackSelection}.
@ -209,12 +216,15 @@ public final class CmcdHeadersFactory {
/** Creates and returns a new {@link ImmutableMap} containing the CMCD HTTP request headers. */
public ImmutableMap<@CmcdConfiguration.HeaderKey String, String> createHttpRequestHeaders() {
ImmutableMap<@CmcdConfiguration.HeaderKey String, String> customData =
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);
CmcdObject.Builder cmcdObject =
new CmcdObject.Builder().setCustomData(customData.get(CmcdConfiguration.KEY_CMCD_OBJECT));
CmcdObject.Builder cmcdObject = new CmcdObject.Builder();
if (!getIsInitSegment()) {
if (cmcdConfiguration.isBitrateLoggingAllowed()) {
cmcdObject.setBitrateKbps(bitrateKbps);
@ -231,13 +241,14 @@ public final class CmcdHeadersFactory {
cmcdObject.setObjectDurationMs(Util.usToMs(chunkDurationUs));
}
}
if (cmcdConfiguration.isObjectTypeLoggingAllowed()) {
cmcdObject.setObjectType(objectType);
}
if (customData.containsKey(CmcdConfiguration.KEY_CMCD_OBJECT)) {
cmcdObject.setCustomDataList(customData.get(CmcdConfiguration.KEY_CMCD_OBJECT));
}
CmcdRequest.Builder cmcdRequest =
new CmcdRequest.Builder().setCustomData(customData.get(CmcdConfiguration.KEY_CMCD_REQUEST));
CmcdRequest.Builder cmcdRequest = new CmcdRequest.Builder();
if (!getIsInitSegment() && cmcdConfiguration.isBufferLengthLoggingAllowed()) {
cmcdRequest.setBufferLengthMs(Util.usToMs(bufferedDurationUs));
}
@ -252,9 +263,11 @@ public final class CmcdHeadersFactory {
if (cmcdConfiguration.isStartupLoggingAllowed()) {
cmcdRequest.setStartup(didRebuffer || isBufferEmpty);
}
if (customData.containsKey(CmcdConfiguration.KEY_CMCD_REQUEST)) {
cmcdRequest.setCustomDataList(customData.get(CmcdConfiguration.KEY_CMCD_REQUEST));
}
CmcdSession.Builder cmcdSession =
new CmcdSession.Builder().setCustomData(customData.get(CmcdConfiguration.KEY_CMCD_SESSION));
CmcdSession.Builder cmcdSession = new CmcdSession.Builder();
if (cmcdConfiguration.isContentIdLoggingAllowed()) {
cmcdSession.setContentId(cmcdConfiguration.contentId);
}
@ -270,9 +283,11 @@ public final class CmcdHeadersFactory {
if (cmcdConfiguration.isPlaybackRateLoggingAllowed()) {
cmcdSession.setPlaybackRate(playbackRate);
}
if (customData.containsKey(CmcdConfiguration.KEY_CMCD_SESSION)) {
cmcdSession.setCustomDataList(customData.get(CmcdConfiguration.KEY_CMCD_SESSION));
}
CmcdStatus.Builder cmcdStatus =
new CmcdStatus.Builder().setCustomData(customData.get(CmcdConfiguration.KEY_CMCD_STATUS));
CmcdStatus.Builder cmcdStatus = new CmcdStatus.Builder();
if (cmcdConfiguration.isMaximumRequestThroughputLoggingAllowed()) {
cmcdStatus.setMaximumRequestedThroughputKbps(
cmcdConfiguration.requestConfig.getRequestedMaximumThroughputKbps(bitrateKbps));
@ -280,6 +295,9 @@ public final class CmcdHeadersFactory {
if (cmcdConfiguration.isBufferStarvationLoggingAllowed()) {
cmcdStatus.setBufferStarvation(didRebuffer);
}
if (customData.containsKey(CmcdConfiguration.KEY_CMCD_STATUS)) {
cmcdStatus.setCustomDataList(customData.get(CmcdConfiguration.KEY_CMCD_STATUS));
}
ImmutableMap.Builder<String, String> httpRequestHeaders = ImmutableMap.builder();
cmcdObject.build().populateHttpRequestHeaders(httpRequestHeaders);
@ -293,6 +311,13 @@ public final class CmcdHeadersFactory {
return objectType != null && objectType.equals(OBJECT_TYPE_INIT_SEGMENT);
}
private void validateCustomDataListFormat(List<String> customDataList) {
for (String customData : customDataList) {
String key = Util.split(customData, "=")[0];
checkState(CUSTOM_KEY_NAME_PATTERN.matcher(key).matches());
}
}
/**
* Keys whose values vary with the object being requested. Contains CMCD fields: {@code br},
* {@code tb}, {@code d} and {@code ot}.
@ -305,13 +330,14 @@ public final class CmcdHeadersFactory {
private int topBitrateKbps;
private long objectDurationMs;
@Nullable private @ObjectType String objectType;
@Nullable private String customData;
private ImmutableList<String> customDataList;
/** Creates a new instance with default values. */
public Builder() {
this.bitrateKbps = C.RATE_UNSET_INT;
this.topBitrateKbps = C.RATE_UNSET_INT;
this.objectDurationMs = C.TIME_UNSET;
this.customDataList = ImmutableList.of();
}
/**
@ -360,10 +386,10 @@ public final class CmcdHeadersFactory {
return this;
}
/** Sets the {@link CmcdObject#customData}. The default value is {@code null}. */
/** Sets the {@link CmcdObject#customDataList}. The default value is an empty list. */
@CanIgnoreReturnValue
public Builder setCustomData(@Nullable String customData) {
this.customData = customData;
public Builder setCustomDataList(List<String> customDataList) {
this.customDataList = ImmutableList.copyOf(customDataList);
return this;
}
@ -405,21 +431,15 @@ public final class CmcdHeadersFactory {
*/
@Nullable public final @ObjectType String objectType;
/**
* Custom data where the values of the keys vary with the object being requested, or {@code
* null} if unset.
*
* <p>The String consists of key-value pairs separated by commas.<br>
* Example: {@code key1=intValue,key2="stringValue"}.
*/
@Nullable public final String customData;
/** Custom data that vary based on the specific object being requested. */
public final ImmutableList<String> customDataList;
private CmcdObject(Builder builder) {
this.bitrateKbps = builder.bitrateKbps;
this.topBitrateKbps = builder.topBitrateKbps;
this.objectDurationMs = builder.objectDurationMs;
this.objectType = builder.objectType;
this.customData = builder.customData;
this.customDataList = builder.customDataList;
}
/**
@ -443,9 +463,7 @@ public final class CmcdHeadersFactory {
if (!TextUtils.isEmpty(objectType)) {
headerValueList.add(CmcdConfiguration.KEY_OBJECT_TYPE + "=" + objectType);
}
if (!TextUtils.isEmpty(customData)) {
headerValueList.add(customData);
}
headerValueList.addAll(customDataList);
if (!headerValueList.isEmpty()) {
httpRequestHeaders.put(
@ -466,13 +484,14 @@ public final class CmcdHeadersFactory {
private long measuredThroughputInKbps;
private long deadlineMs;
private boolean startup;
@Nullable private String customData;
private ImmutableList<String> customDataList;
/** Creates a new instance with default values. */
public Builder() {
this.bufferLengthMs = C.TIME_UNSET;
this.measuredThroughputInKbps = C.RATE_UNSET_INT;
this.deadlineMs = C.TIME_UNSET;
this.customDataList = ImmutableList.of();
}
/**
@ -526,10 +545,10 @@ public final class CmcdHeadersFactory {
return this;
}
/** Sets the {@link CmcdRequest#customData}. The default value is {@code null}. */
/** Sets the {@link CmcdRequest#customDataList}. The default value is an empty list. */
@CanIgnoreReturnValue
public Builder setCustomData(@Nullable String customData) {
this.customData = customData;
public Builder setCustomDataList(List<String> customDataList) {
this.customDataList = ImmutableList.copyOf(customDataList);
return this;
}
@ -580,20 +599,15 @@ public final class CmcdHeadersFactory {
*/
public final boolean startup;
/**
* Custom data where the values of the keys vary with each request, or {@code null} if unset.
*
* <p>The String consists of key-value pairs separated by commas.<br>
* Example: {@code key1=intValue, key2="stringValue"}.
*/
@Nullable public final String customData;
/** Custom data that vary with each request. */
public final ImmutableList<String> customDataList;
private CmcdRequest(Builder builder) {
this.bufferLengthMs = builder.bufferLengthMs;
this.measuredThroughputInKbps = builder.measuredThroughputInKbps;
this.deadlineMs = builder.deadlineMs;
this.startup = builder.startup;
this.customData = builder.customData;
this.customDataList = builder.customDataList;
}
/**
@ -618,9 +632,7 @@ public final class CmcdHeadersFactory {
if (startup) {
headerValueList.add(CmcdConfiguration.KEY_STARTUP);
}
if (!TextUtils.isEmpty(customData)) {
headerValueList.add(customData);
}
headerValueList.addAll(customDataList);
if (!headerValueList.isEmpty()) {
httpRequestHeaders.put(
@ -642,7 +654,12 @@ public final class CmcdHeadersFactory {
@Nullable private @StreamingFormat String streamingFormat;
@Nullable private @StreamType String streamType;
private float playbackRate;
@Nullable private String customData;
private ImmutableList<String> customDataList;
/** Creates a new instance with default values. */
public Builder() {
this.customDataList = ImmutableList.of();
}
/**
* Sets the {@link CmcdSession#contentId}. Maximum length allowed is 64 characters. The
@ -699,10 +716,10 @@ public final class CmcdHeadersFactory {
return this;
}
/** Sets the {@link CmcdSession#customData}. The default value is {@code null}. */
/** Sets the {@link CmcdSession#customDataList}. The default value is an empty list. */
@CanIgnoreReturnValue
public Builder setCustomData(@Nullable String customData) {
this.customData = customData;
public Builder setCustomDataList(List<String> customDataList) {
this.customDataList = ImmutableList.copyOf(customDataList);
return this;
}
@ -752,14 +769,8 @@ public final class CmcdHeadersFactory {
*/
public final float playbackRate;
/**
* Custom data where the values of the keys are expected to be invariant over the life of the
* session, or {@code null} if unset.
*
* <p>The String consists of key-value pairs separated by commas.<br>
* Example: {@code key1=intValue, key2="stringValue"}.
*/
@Nullable public final String customData;
/** Custom data that is expected to be invariant over the life of the session. */
public final ImmutableList<String> customDataList;
private CmcdSession(Builder builder) {
this.contentId = builder.contentId;
@ -767,7 +778,7 @@ public final class CmcdHeadersFactory {
this.streamingFormat = builder.streamingFormat;
this.streamType = builder.streamType;
this.playbackRate = builder.playbackRate;
this.customData = builder.customData;
this.customDataList = builder.customDataList;
}
/**
@ -800,9 +811,7 @@ public final class CmcdHeadersFactory {
if (VERSION != 1) {
headerValueList.add(CmcdConfiguration.KEY_VERSION + "=" + VERSION);
}
if (!TextUtils.isEmpty(customData)) {
headerValueList.add(customData);
}
headerValueList.addAll(customDataList);
if (!headerValueList.isEmpty()) {
httpRequestHeaders.put(
@ -821,11 +830,12 @@ public final class CmcdHeadersFactory {
public static final class Builder {
private int maximumRequestedThroughputKbps;
private boolean bufferStarvation;
@Nullable private String customData;
private ImmutableList<String> customDataList;
/** Creates a new instance with default values. */
public Builder() {
this.maximumRequestedThroughputKbps = C.RATE_UNSET_INT;
this.customDataList = ImmutableList.of();
}
/**
@ -856,10 +866,10 @@ public final class CmcdHeadersFactory {
return this;
}
/** Sets the {@link CmcdStatus#customData}. The default value is {@code null}. */
/** Sets the {@link CmcdStatus#customDataList}. The default value is an empty list. */
@CanIgnoreReturnValue
public Builder setCustomData(@Nullable String customData) {
this.customData = customData;
public Builder setCustomDataList(List<String> customDataList) {
this.customDataList = ImmutableList.copyOf(customDataList);
return this;
}
@ -882,19 +892,13 @@ public final class CmcdHeadersFactory {
*/
public final boolean bufferStarvation;
/**
* Custom data where the values of the keys do not vary with every request or object, or {@code
* null} if unset.
*
* <p>The String consists of key-value pairs separated by commas.<br>
* Example: {@code key1=intValue, key2="stringValue"}.
*/
@Nullable public final String customData;
/** Custom data that do not vary with every request or object. */
public final ImmutableList<String> customDataList;
private CmcdStatus(Builder builder) {
this.maximumRequestedThroughputKbps = builder.maximumRequestedThroughputKbps;
this.bufferStarvation = builder.bufferStarvation;
this.customData = builder.customData;
this.customDataList = builder.customDataList;
}
/**
@ -913,9 +917,7 @@ public final class CmcdHeadersFactory {
if (bufferStarvation) {
headerValueList.add(CmcdConfiguration.KEY_BUFFER_STARVATION);
}
if (!TextUtils.isEmpty(customData)) {
headerValueList.add(customData);
}
headerValueList.addAll(customDataList);
if (!headerValueList.isEmpty()) {
httpRequestHeaders.put(

View File

@ -21,7 +21,7 @@ import static org.junit.Assert.assertThrows;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableListMultimap;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -97,11 +97,12 @@ public class CmcdConfigurationTest {
}
@Override
public ImmutableMap<@CmcdConfiguration.HeaderKey String, String> getCustomData() {
return new ImmutableMap.Builder<String, String>()
.put("CMCD-Object", "key1=value1")
.put("CMCD-Request", "key2=\"stringValue\"")
.buildOrThrow();
public ImmutableListMultimap<@CmcdConfiguration.HeaderKey String, String>
getCustomData() {
return new ImmutableListMultimap.Builder<String, String>()
.putAll("CMCD-Object", "key-1=1", "key-2=2")
.put("CMCD-Request", "key-3=\"stringValue1,stringValue2\"")
.build();
}
@Override
@ -121,8 +122,13 @@ public class CmcdConfigurationTest {
assertThat(cmcdConfiguration.isContentIdLoggingAllowed()).isFalse();
assertThat(cmcdConfiguration.isSessionIdLoggingAllowed()).isFalse();
assertThat(cmcdConfiguration.isMaximumRequestThroughputLoggingAllowed()).isTrue();
assertThat(cmcdConfiguration.requestConfig.getCustomData().keySet()).hasSize(2);
assertThat(cmcdConfiguration.requestConfig.getCustomData())
.containsExactly("CMCD-Object", "key1=value1", "CMCD-Request", "key2=\"stringValue\"");
.valuesForKey(CmcdConfiguration.KEY_CMCD_OBJECT)
.containsExactly("key-1=1", "key-2=2");
assertThat(cmcdConfiguration.requestConfig.getCustomData())
.valuesForKey(CmcdConfiguration.KEY_CMCD_REQUEST)
.containsExactly("key-3=\"stringValue1,stringValue2\"");
assertThat(
cmcdConfiguration.requestConfig.getRequestedMaximumThroughputKbps(
/* throughputKbps= */ 100))

View File

@ -16,6 +16,7 @@
package androidx.media3.exoplayer.upstream;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@ -24,6 +25,7 @@ import androidx.media3.common.MediaItem;
import androidx.media3.common.TrackGroup;
import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableMap;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -41,11 +43,13 @@ public class CmcdHeadersFactoryTest {
mediaItem.mediaId,
new CmcdConfiguration.RequestConfig() {
@Override
public ImmutableMap<@CmcdConfiguration.HeaderKey String, String> getCustomData() {
return new ImmutableMap.Builder<String, String>()
.put("CMCD-Object", "key1=value1")
.put("CMCD-Request", "key2=\"stringValue\"")
.buildOrThrow();
public ImmutableListMultimap<@CmcdConfiguration.HeaderKey String, String>
getCustomData() {
return new ImmutableListMultimap.Builder<String, String>()
.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
@ -79,12 +83,47 @@ public class CmcdHeadersFactoryTest {
assertThat(requestHeaders)
.containsExactly(
"CMCD-Object",
"br=840,tb=1000,d=3000,key1=value1",
"br=840,tb=1000,d=3000,key-1=1,key-2-separated-by-multiple-hyphens=2",
"CMCD-Request",
"bl=1800,mtp=500,dl=900,su,key2=\"stringValue\"",
"bl=1800,mtp=500,dl=900,su,key-3=\"stringValue1,stringValue2\"",
"CMCD-Session",
"cid=\"mediaId\",sid=\"sessionId\",sf=d,st=l,pr=2.00",
"CMCD-Status",
"rtp=1700,bs");
"rtp=1700,bs,key-4=\"stringValue3=stringValue4\"");
}
@Test
public void createInstance_withInvalidNonHyphenatedCustomKey_throwsIllegalStateException() {
CmcdConfiguration.Factory cmcdConfigurationFactory =
mediaItem ->
new CmcdConfiguration(
null,
null,
new CmcdConfiguration.RequestConfig() {
@Override
public ImmutableListMultimap<@CmcdConfiguration.HeaderKey String, String>
getCustomData() {
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());
assertThrows(
IllegalStateException.class,
() ->
new CmcdHeadersFactory(
cmcdConfiguration,
trackSelection,
/* bufferedDurationUs= */ 0,
/* playbackRate= */ 1.0f,
/* streamingFormat= */ CmcdHeadersFactory.STREAMING_FORMAT_DASH,
/* isLive= */ true,
/* didRebuffer= */ true,
/* isBufferEmpty= */ false)
.createHttpRequestHeaders());
}
}

View File

@ -52,6 +52,7 @@ import androidx.media3.test.utils.TestUtil;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import java.io.IOException;
@ -435,13 +436,15 @@ public class DefaultDashChunkSourceTest {
CmcdConfiguration.RequestConfig cmcdRequestConfig =
new CmcdConfiguration.RequestConfig() {
@Override
public ImmutableMap<@CmcdConfiguration.HeaderKey String, String> getCustomData() {
return new ImmutableMap.Builder<@CmcdConfiguration.HeaderKey String, String>()
.put(CmcdConfiguration.KEY_CMCD_OBJECT, "key1=value1")
.put(CmcdConfiguration.KEY_CMCD_REQUEST, "key2=\"stringValue\"")
.put(CmcdConfiguration.KEY_CMCD_SESSION, "key3=1")
.put(CmcdConfiguration.KEY_CMCD_STATUS, "key4=5.0")
.buildOrThrow();
public ImmutableListMultimap<@CmcdConfiguration.HeaderKey String, String>
getCustomData() {
return new ImmutableListMultimap.Builder<
@CmcdConfiguration.HeaderKey String, String>()
.put(CmcdConfiguration.KEY_CMCD_OBJECT, "key-1=1")
.put(CmcdConfiguration.KEY_CMCD_REQUEST, "key-2=\"stringValue\"")
.put(CmcdConfiguration.KEY_CMCD_SESSION, "key-3=3")
.put(CmcdConfiguration.KEY_CMCD_STATUS, "key-4=5.0")
.build();
}
};
@ -463,13 +466,13 @@ public class DefaultDashChunkSourceTest {
assertThat(output.chunk.dataSpec.httpRequestHeaders)
.containsExactly(
"CMCD-Object",
"br=700,tb=1300,d=4000,ot=v,key1=value1",
"br=700,tb=1300,d=4000,ot=v,key-1=1",
"CMCD-Request",
"bl=0,mtp=1000,dl=0,su,key2=\"stringValue\"",
"bl=0,mtp=1000,dl=0,su,key-2=\"stringValue\"",
"CMCD-Session",
"cid=\"mediaId\",sid=\"" + cmcdConfiguration.sessionId + "\",sf=d,st=v,key3=1",
"cid=\"mediaId\",sid=\"" + cmcdConfiguration.sessionId + "\",sf=d,st=v,key-3=3",
"CMCD-Status",
"key4=5.0");
"key-4=5.0");
}
@Test

View File

@ -40,7 +40,7 @@ import androidx.media3.test.utils.TestUtil;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableListMultimap;
import java.io.IOException;
import java.io.InputStream;
import java.time.Duration;
@ -345,13 +345,15 @@ public class HlsChunkSourceTest {
CmcdConfiguration.RequestConfig cmcdRequestConfig =
new CmcdConfiguration.RequestConfig() {
@Override
public ImmutableMap<@CmcdConfiguration.HeaderKey String, String> getCustomData() {
return new ImmutableMap.Builder<@CmcdConfiguration.HeaderKey String, String>()
.put(CmcdConfiguration.KEY_CMCD_OBJECT, "key1=value1")
.put(CmcdConfiguration.KEY_CMCD_REQUEST, "key2=\"stringValue\"")
.put(CmcdConfiguration.KEY_CMCD_SESSION, "key3=1")
.put(CmcdConfiguration.KEY_CMCD_STATUS, "key4=5.0")
.buildOrThrow();
public ImmutableListMultimap<@CmcdConfiguration.HeaderKey String, String>
getCustomData() {
return new ImmutableListMultimap.Builder<
@CmcdConfiguration.HeaderKey String, String>()
.put(CmcdConfiguration.KEY_CMCD_OBJECT, "key-1=1")
.put(CmcdConfiguration.KEY_CMCD_REQUEST, "key-2=\"stringValue\"")
.put(CmcdConfiguration.KEY_CMCD_SESSION, "key-3=3")
.put(CmcdConfiguration.KEY_CMCD_STATUS, "key-4=5.0")
.build();
}
};
@ -374,13 +376,13 @@ public class HlsChunkSourceTest {
assertThat(output.chunk.dataSpec.httpRequestHeaders)
.containsExactly(
"CMCD-Object",
"br=800,tb=800,d=4000,ot=v,key1=value1",
"br=800,tb=800,d=4000,ot=v,key-1=1",
"CMCD-Request",
"bl=0,dl=0,su,key2=\"stringValue\"",
"bl=0,dl=0,su,key-2=\"stringValue\"",
"CMCD-Session",
"cid=\"mediaId\",sid=\"" + cmcdConfiguration.sessionId + "\",sf=h,st=v,key3=1",
"cid=\"mediaId\",sid=\"" + cmcdConfiguration.sessionId + "\",sf=h,st=v,key-3=3",
"CMCD-Status",
"key4=5.0");
"key-4=5.0");
}
private HlsChunkSource createHlsChunkSource(@Nullable CmcdConfiguration cmcdConfiguration) {

View File

@ -38,7 +38,7 @@ import androidx.media3.test.utils.TestUtil;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableListMultimap;
import java.io.IOException;
import java.time.Duration;
import org.junit.Test;
@ -186,13 +186,15 @@ public class DefaultSsChunkSourceTest {
CmcdConfiguration.RequestConfig cmcdRequestConfig =
new CmcdConfiguration.RequestConfig() {
@Override
public ImmutableMap<@CmcdConfiguration.HeaderKey String, String> getCustomData() {
return new ImmutableMap.Builder<@CmcdConfiguration.HeaderKey String, String>()
.put(CmcdConfiguration.KEY_CMCD_OBJECT, "key1=value1")
.put(CmcdConfiguration.KEY_CMCD_REQUEST, "key2=\"stringValue\"")
.put(CmcdConfiguration.KEY_CMCD_SESSION, "key3=1")
.put(CmcdConfiguration.KEY_CMCD_STATUS, "key4=5.0")
.buildOrThrow();
public ImmutableListMultimap<@CmcdConfiguration.HeaderKey String, String>
getCustomData() {
return new ImmutableListMultimap.Builder<
@CmcdConfiguration.HeaderKey String, String>()
.put(CmcdConfiguration.KEY_CMCD_OBJECT, "key-1=1")
.put(CmcdConfiguration.KEY_CMCD_REQUEST, "key-2=\"stringValue\"")
.put(CmcdConfiguration.KEY_CMCD_SESSION, "key-3=3")
.put(CmcdConfiguration.KEY_CMCD_STATUS, "key-4=5.0")
.build();
}
};
@ -214,13 +216,13 @@ public class DefaultSsChunkSourceTest {
assertThat(output.chunk.dataSpec.httpRequestHeaders)
.containsExactly(
"CMCD-Object",
"br=308,tb=1536,d=1968,ot=v,key1=value1",
"br=308,tb=1536,d=1968,ot=v,key-1=1",
"CMCD-Request",
"bl=0,mtp=1000,dl=0,su,key2=\"stringValue\"",
"bl=0,mtp=1000,dl=0,su,key-2=\"stringValue\"",
"CMCD-Session",
"cid=\"mediaId\",sid=\"" + cmcdConfiguration.sessionId + "\",sf=s,st=v,key3=1",
"cid=\"mediaId\",sid=\"" + cmcdConfiguration.sessionId + "\",sf=s,st=v,key-3=3",
"CMCD-Status",
"key4=5.0");
"key-4=5.0");
}
private SsChunkSource createSsChunkSource(