Enable sending CmcdData for manifest requests in DASH, HLS and SS

Issue: androidx/media#1951
PiperOrigin-RevId: 704875765
This commit is contained in:
rohks 2024-12-10 15:56:04 -08:00 committed by Copybara-Service
parent c377a34a5a
commit 8d2f531470
12 changed files with 568 additions and 192 deletions

View File

@ -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`.

View File

@ -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.
*
* <p>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}.
*
* <p>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.
*
* <p>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.
*
* <p>Must be set to a non-null value if the {@link #setObjectType(String)} is not {@link
* #OBJECT_TYPE_MANIFEST}
*
* <p>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.
*
* <p>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}.
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p>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<String> 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.
*
* <p>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;
}

View File

@ -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<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
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<String, String>()
.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<String, String>()
.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<String, String>()
.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());
}
}

View File

@ -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));
}

View File

@ -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(

View File

@ -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

View File

@ -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,

View File

@ -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<Uri, MediaPlaylistBundle> playlistBundles;
private final CopyOnWriteArrayList<PlaylistEventListener> 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<HlsPlaylist> 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<HlsPlaylist> 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<HlsPlaylist> mediaPlaylistLoadable =
new ParsingLoadable<>(
mediaPlaylistDataSource,
playlistRequestUri,
C.DATA_TYPE_MANIFEST,
mediaPlaylistParser);
mediaPlaylistDataSource, dataSpec, C.DATA_TYPE_MANIFEST, mediaPlaylistParser);
mediaPlaylistLoader.startLoading(
mediaPlaylistLoadable,
/* callback= */ this,

View File

@ -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. */

View File

@ -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<TimeoutException> 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<HlsMediaPlaylist> mediaPlaylists = new ArrayList<>();
AtomicInteger playlistCounter = new AtomicInteger();
AtomicReference<TimeoutException> 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<HlsMediaPlaylist> mediaPlaylists = new ArrayList<>();
AtomicInteger playlistCounter = new AtomicInteger();
AtomicReference<TimeoutException> playlistRefreshExceptionRef = new AtomicReference<>();
@ -674,7 +677,8 @@ public class DefaultHlsPlaylistTrackerTest {
new DefaultHlsPlaylistTracker(
dataType -> dataSourceFactory.createDataSource(),
new DefaultLoadErrorHandlingPolicy(),
new DefaultHlsPlaylistParserFactory());
new DefaultHlsPlaylistParserFactory(),
/* cmcdConfiguration= */ null);
List<HlsMediaPlaylist> mediaPlaylists = new ArrayList<>();
AtomicInteger playlistCounter = new AtomicInteger();

View File

@ -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));

View File

@ -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<SsManifest> 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));
}