From afb8d6c9e24be5e76df9964efc99f8303dd78bdc Mon Sep 17 00:00:00 2001 From: rohks Date: Sat, 19 Aug 2023 01:40:38 +0100 Subject: [PATCH] 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`. This would enable us to sort all keys to reduce the fingerprinting surface. PiperOrigin-RevId: 558291240 --- .../exoplayer/upstream/CmcdConfiguration.java | 39 +++-- .../upstream/CmcdHeadersFactory.java | 148 +++++++++--------- .../upstream/CmcdConfigurationTest.java | 20 ++- .../upstream/CmcdHeadersFactoryTest.java | 55 ++++++- .../dash/DefaultDashChunkSourceTest.java | 25 +-- .../exoplayer/hls/HlsChunkSourceTest.java | 26 +-- .../DefaultSsChunkSourceTest.java | 26 +-- 7 files changed, 204 insertions(+), 135 deletions(-) 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 c39999a1a2..8d82138719 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 @@ -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 { * *

By default, no custom data is provided. * - *

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. + *

The data payload consists of a series of key/value pairs constructed according to the + * following rules: + * + *

+ * + *

Note: The key words MUST and SHOULD are to be interpreted as described in RFC 2119. * *

Example: * *

* - * @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(); } /** 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 24e7d52858..11b860f9b1 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 @@ -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 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 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 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 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. - * - *

The String consists of key-value pairs separated by commas.
- * 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 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 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 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. - * - *

The String consists of key-value pairs separated by commas.
- * Example: {@code key1=intValue, key2="stringValue"}. - */ - @Nullable public final String customData; + /** Custom data that vary with each request. */ + public final ImmutableList 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 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 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. - * - *

The String consists of key-value pairs separated by commas.
- * 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 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 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 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. - * - *

The String consists of key-value pairs separated by commas.
- * 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 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( diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/CmcdConfigurationTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/CmcdConfigurationTest.java index 9187fb6dfd..2f3623dcf3 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/CmcdConfigurationTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/CmcdConfigurationTest.java @@ -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() - .put("CMCD-Object", "key1=value1") - .put("CMCD-Request", "key2=\"stringValue\"") - .buildOrThrow(); + public ImmutableListMultimap<@CmcdConfiguration.HeaderKey String, String> + getCustomData() { + return new ImmutableListMultimap.Builder() + .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)) diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/CmcdHeadersFactoryTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/CmcdHeadersFactoryTest.java index bb37cde1be..92d1b191d8 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/CmcdHeadersFactoryTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/CmcdHeadersFactoryTest.java @@ -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() - .put("CMCD-Object", "key1=value1") - .put("CMCD-Request", "key2=\"stringValue\"") - .buildOrThrow(); + 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 @@ -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()); } } 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 f8dfaa685a..a0811c907b 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 @@ -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 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 6d2e78cf78..bfb1a44976 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 @@ -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) { 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 fe862051f3..5391560dd0 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 @@ -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(