From 8d2f531470d7437d8ddf01b8fca196d1106a8cdb Mon Sep 17 00:00:00 2001 From: rohks Date: Tue, 10 Dec 2024 15:56:04 -0800 Subject: [PATCH] Enable sending `CmcdData` for manifest requests in DASH, HLS and SS Issue: androidx/media#1951 PiperOrigin-RevId: 704875765 --- RELEASENOTES.md | 3 + .../media3/exoplayer/upstream/CmcdData.java | 286 +++++++++++----- .../exoplayer/upstream/CmcdDataTest.java | 322 ++++++++++++++---- .../exoplayer/dash/DashMediaSource.java | 15 +- .../dash/DefaultDashChunkSource.java | 18 +- .../media3/exoplayer/hls/HlsChunkSource.java | 17 +- .../media3/exoplayer/hls/HlsMediaSource.java | 5 +- .../playlist/DefaultHlsPlaylistTracker.java | 45 ++- .../hls/playlist/HlsPlaylistTracker.java | 5 +- .../DefaultHlsPlaylistTrackerTest.java | 12 +- .../smoothstreaming/DefaultSsChunkSource.java | 16 +- .../smoothstreaming/SsMediaSource.java | 16 +- 12 files changed, 568 insertions(+), 192 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index d552dc269c..fff06b960d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -27,6 +27,9 @@ * `RenderersFactory.createSecondaryRenderer` can be implemented to provide secondary renderers for pre-warming. Pre-warming enables quicker media item transitions during playback. + * Enable sending `CmcdData` for manifest requests in adaptive streaming + formats DASH, HLS, and SmoothStreaming + ([#1951](https://github.com/androidx/media/issues/1951)). * Transformer: * Update parameters of `VideoFrameProcessor.registerInputStream` and `VideoFrameProcessor.Listener.onInputStreamRegistered` to use `Format`. diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/CmcdData.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/CmcdData.java index 3ec0717f01..3c8151badb 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/CmcdData.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/CmcdData.java @@ -16,7 +16,9 @@ package androidx.media3.exoplayer.upstream; import static androidx.media3.common.util.Assertions.checkArgument; +import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkState; +import static androidx.media3.common.util.Assertions.checkStateNotNull; import static java.lang.Math.max; import static java.lang.annotation.ElementType.TYPE_USE; @@ -46,6 +48,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.regex.Pattern; /** @@ -89,6 +92,9 @@ public final class CmcdData { /** Represents the object type for muxed audio and video content in a media container. */ public static final String OBJECT_TYPE_MUXED_AUDIO_AND_VIDEO = "av"; + /** Represents the object type for a manifest or playlist file, in a media container. */ + public static final String OBJECT_TYPE_MANIFEST = "m"; + /** * Custom key names MUST carry a hyphenated prefix to ensure that there will not be a namespace * collision with future revisions to this specification. Clients SHOULD use a reverse-DNS @@ -97,13 +103,13 @@ public final class CmcdData { private static final Pattern CUSTOM_KEY_NAME_PATTERN = Pattern.compile(".*-.*"); private final CmcdConfiguration cmcdConfiguration; - private final ExoTrackSelection trackSelection; - private final long bufferedDurationUs; - private final float playbackRate; private final @CmcdData.StreamingFormat String streamingFormat; - private final boolean isLive; - private final boolean didRebuffer; - private final boolean isBufferEmpty; + @Nullable private ExoTrackSelection trackSelection; + private long bufferedDurationUs; + private float playbackRate; + @Nullable private Boolean isLive; + private boolean didRebuffer; + private boolean isBufferEmpty; private long chunkDurationUs; @Nullable private @CmcdData.ObjectType String objectType; @Nullable private String nextObjectRequest; @@ -112,41 +118,15 @@ public final class CmcdData { /** * Creates an instance. * - * @param cmcdConfiguration The {@link CmcdConfiguration} for this chunk source. - * @param trackSelection The {@linkplain ExoTrackSelection track selection}. - * @param bufferedDurationUs The duration of media currently buffered from the current playback - * position, in microseconds. - * @param playbackRate The playback rate indicating the current speed of playback. - * @param streamingFormat The streaming format of the media content. Must be one of the allowed - * streaming formats specified by the {@link CmcdData.StreamingFormat} annotation. - * @param isLive {@code true} if the media content is being streamed live, {@code false} - * otherwise. - * @param didRebuffer {@code true} if a rebuffering event happened between the previous request - * and this one, {@code false} otherwise. - * @param isBufferEmpty {@code true} if the queue of buffered chunks is empty, {@code false} - * otherwise. - * @throws IllegalArgumentException If {@code bufferedDurationUs} is negative or {@code - * playbackRate} is non-positive. + * @param cmcdConfiguration The {@link CmcdConfiguration} for this source. + * @param streamingFormat The streaming format of the media content. */ public Factory( - CmcdConfiguration cmcdConfiguration, - ExoTrackSelection trackSelection, - long bufferedDurationUs, - float playbackRate, - @CmcdData.StreamingFormat String streamingFormat, - boolean isLive, - boolean didRebuffer, - boolean isBufferEmpty) { - checkArgument(bufferedDurationUs >= 0); - checkArgument(playbackRate == C.RATE_UNSET || playbackRate > 0); + CmcdConfiguration cmcdConfiguration, @CmcdData.StreamingFormat String streamingFormat) { this.cmcdConfiguration = cmcdConfiguration; - this.trackSelection = trackSelection; - this.bufferedDurationUs = bufferedDurationUs; - this.playbackRate = playbackRate; + this.bufferedDurationUs = C.TIME_UNSET; + this.playbackRate = C.RATE_UNSET; this.streamingFormat = streamingFormat; - this.isLive = isLive; - this.didRebuffer = didRebuffer; - this.isBufferEmpty = isBufferEmpty; this.chunkDurationUs = C.TIME_UNSET; } @@ -160,7 +140,6 @@ public final class CmcdData { */ @Nullable public static @CmcdData.ObjectType String getObjectType(ExoTrackSelection trackSelection) { - checkArgument(trackSelection != null); @TrackType int trackType = MimeTypes.getTrackType(trackSelection.getSelectedFormat().sampleMimeType); if (trackType == C.TRACK_TYPE_UNKNOWN) { @@ -178,8 +157,13 @@ public final class CmcdData { } /** - * Sets the duration of current media chunk being requested, in microseconds. The default value - * is {@link C#TIME_UNSET}. + * Sets the duration of current media chunk being requested, in microseconds. + * + *

Must be set to a non-negative value if the {@linkplain #setObjectType(String) object type} + * is set and one of {@link #OBJECT_TYPE_AUDIO_ONLY}, {@link #OBJECT_TYPE_VIDEO_ONLY} or {@link + * #OBJECT_TYPE_MUXED_AUDIO_AND_VIDEO}. + * + *

Default value is {@link C#TIME_UNSET}. * * @throws IllegalArgumentException If {@code chunkDurationUs} is negative. */ @@ -191,8 +175,7 @@ public final class CmcdData { } /** - * Sets the object type of the current object being requested. Must be one of the allowed object - * types specified by the {@link CmcdData.ObjectType} annotation. + * Sets the object type of the current object being requested. * *

Default is {@code null}. */ @@ -226,31 +209,145 @@ public final class CmcdData { return this; } + /** + * Sets the {@linkplain ExoTrackSelection track selection} for the media being played. + * + *

Must be set to a non-null value if the {@link #setObjectType(String)} is not {@link + * #OBJECT_TYPE_MANIFEST} + * + *

Default is {@code null}. + */ + @CanIgnoreReturnValue + public Factory setTrackSelection(ExoTrackSelection trackSelection) { + this.trackSelection = trackSelection; + return this; + } + + /** + * Sets the duration of media currently buffered from the current playback position, in + * microseconds. + * + *

Must be set to a non-negative value if the {@linkplain #setObjectType(String) object type} + * is set and one of {@link #OBJECT_TYPE_AUDIO_ONLY}, {@link #OBJECT_TYPE_VIDEO_ONLY} or {@link + * #OBJECT_TYPE_MUXED_AUDIO_AND_VIDEO}. + * + *

Default value is {@link C#TIME_UNSET}. + * + * @throws IllegalArgumentException If {@code bufferedDurationUs} is negative. + */ + @CanIgnoreReturnValue + public Factory setBufferedDurationUs(long bufferedDurationUs) { + checkArgument(bufferedDurationUs >= 0); + this.bufferedDurationUs = bufferedDurationUs; + return this; + } + + /** + * Sets the playback rate indicating the current speed of playback. + * + *

Default value is {@link C#RATE_UNSET}. + * + * @throws IllegalArgumentException If {@code playbackRate} is non-positive and not {@link + * C#RATE_UNSET}. + */ + @CanIgnoreReturnValue + public Factory setPlaybackRate(float playbackRate) { + checkArgument(playbackRate == C.RATE_UNSET || playbackRate > 0); + this.playbackRate = playbackRate; + return this; + } + + /** + * Sets whether the media content is being streamed live. + * + *

Default value is {@code false}. + */ + @CanIgnoreReturnValue + public Factory setIsLive(boolean isLive) { + this.isLive = isLive; + return this; + } + + /** + * Sets whether a rebuffering event occurred between the previous request and this one. + * + *

Default value is {@code false}. + */ + @CanIgnoreReturnValue + public Factory setDidRebuffer(boolean didRebuffer) { + this.didRebuffer = didRebuffer; + return this; + } + + /** + * Sets whether the queue of buffered chunks is empty. + * + *

Default value is {@code false}. + */ + @CanIgnoreReturnValue + public Factory setIsBufferEmpty(boolean isBufferEmpty) { + this.isBufferEmpty = isBufferEmpty; + return this; + } + + /** + * Creates a {@link CmcdData} instance. + * + * @throws IllegalStateException If any required parameters have not been set. + */ public CmcdData createCmcdData() { + boolean isManifestObjectType = isManifestObjectType(objectType); + boolean isMediaObjectType = isMediaObjectType(objectType); + + if (!isManifestObjectType) { + checkStateNotNull(trackSelection, "Track selection must be set"); + } + if (isMediaObjectType) { + checkState(bufferedDurationUs != C.TIME_UNSET, "Buffered duration must be set"); + checkState(chunkDurationUs != C.TIME_UNSET, "Chunk duration must be set"); + } + ImmutableListMultimap<@CmcdConfiguration.HeaderKey String, String> customData = cmcdConfiguration.requestConfig.getCustomData(); for (String headerKey : customData.keySet()) { validateCustomDataListFormat(customData.get(headerKey)); } - int bitrateKbps = Util.ceilDivide(trackSelection.getSelectedFormat().bitrate, 1000); + int bitrateKbps = C.RATE_UNSET_INT; + int topBitrateKbps = C.RATE_UNSET_INT; + long latestBitrateEstimateKbps = C.RATE_UNSET_INT; + int requestedMaximumThroughputKbps = C.RATE_UNSET_INT; + + if (!isManifestObjectType) { + ExoTrackSelection trackSelection = checkNotNull(this.trackSelection); + int selectedTrackBitrate = trackSelection.getSelectedFormat().bitrate; + bitrateKbps = Util.ceilDivide(selectedTrackBitrate, 1000); + + TrackGroup trackGroup = trackSelection.getTrackGroup(); + int topBitrate = selectedTrackBitrate; + for (int i = 0; i < trackGroup.length; i++) { + topBitrate = max(topBitrate, trackGroup.getFormat(i).bitrate); + } + topBitrateKbps = Util.ceilDivide(topBitrate, 1000); + + if (trackSelection.getLatestBitrateEstimate() != C.RATE_UNSET_INT) { + latestBitrateEstimateKbps = + Util.ceilDivide(trackSelection.getLatestBitrateEstimate(), 1000); + } + + requestedMaximumThroughputKbps = + cmcdConfiguration.requestConfig.getRequestedMaximumThroughputKbps(bitrateKbps); + } CmcdObject.Builder cmcdObject = new CmcdObject.Builder(); - if (!getIsInitSegment()) { - if (cmcdConfiguration.isBitrateLoggingAllowed()) { - cmcdObject.setBitrateKbps(bitrateKbps); - } - if (cmcdConfiguration.isTopBitrateLoggingAllowed()) { - TrackGroup trackGroup = trackSelection.getTrackGroup(); - int topBitrate = trackSelection.getSelectedFormat().bitrate; - for (int i = 0; i < trackGroup.length; i++) { - topBitrate = max(topBitrate, trackGroup.getFormat(i).bitrate); - } - cmcdObject.setTopBitrateKbps(Util.ceilDivide(topBitrate, 1000)); - } - if (cmcdConfiguration.isObjectDurationLoggingAllowed()) { - cmcdObject.setObjectDurationMs(Util.usToMs(chunkDurationUs)); - } + if (cmcdConfiguration.isBitrateLoggingAllowed()) { + cmcdObject.setBitrateKbps(bitrateKbps); + } + if (cmcdConfiguration.isTopBitrateLoggingAllowed()) { + cmcdObject.setTopBitrateKbps(topBitrateKbps); + } + if (isMediaObjectType && cmcdConfiguration.isObjectDurationLoggingAllowed()) { + cmcdObject.setObjectDurationMs(Util.usToMs(chunkDurationUs)); } if (cmcdConfiguration.isObjectTypeLoggingAllowed()) { cmcdObject.setObjectType(objectType); @@ -260,16 +357,16 @@ public final class CmcdData { } CmcdRequest.Builder cmcdRequest = new CmcdRequest.Builder(); - if (!getIsInitSegment() && cmcdConfiguration.isBufferLengthLoggingAllowed()) { - cmcdRequest.setBufferLengthMs(Util.usToMs(bufferedDurationUs)); + if (isMediaObjectType) { + if (cmcdConfiguration.isBufferLengthLoggingAllowed()) { + cmcdRequest.setBufferLengthMs(Util.usToMs(bufferedDurationUs)); + } + if (cmcdConfiguration.isDeadlineLoggingAllowed()) { + cmcdRequest.setDeadlineMs(Util.usToMs((long) (bufferedDurationUs / playbackRate))); + } } - if (cmcdConfiguration.isMeasuredThroughputLoggingAllowed() - && trackSelection.getLatestBitrateEstimate() != C.RATE_UNSET_INT) { - cmcdRequest.setMeasuredThroughputInKbps( - Util.ceilDivide(trackSelection.getLatestBitrateEstimate(), 1000)); - } - if (cmcdConfiguration.isDeadlineLoggingAllowed()) { - cmcdRequest.setDeadlineMs(Util.usToMs((long) (bufferedDurationUs / playbackRate))); + if (cmcdConfiguration.isMeasuredThroughputLoggingAllowed()) { + cmcdRequest.setMeasuredThroughputInKbps(latestBitrateEstimateKbps); } if (cmcdConfiguration.isStartupLoggingAllowed()) { cmcdRequest.setStartup(didRebuffer || isBufferEmpty); @@ -294,8 +391,8 @@ public final class CmcdData { if (cmcdConfiguration.isStreamingFormatLoggingAllowed()) { cmcdSession.setStreamingFormat(streamingFormat); } - if (cmcdConfiguration.isStreamTypeLoggingAllowed()) { - cmcdSession.setStreamType(isLive ? STREAM_TYPE_LIVE : STREAM_TYPE_VOD); + if (isLive != null && cmcdConfiguration.isStreamTypeLoggingAllowed()) { + cmcdSession.setStreamType(checkNotNull(isLive) ? STREAM_TYPE_LIVE : STREAM_TYPE_VOD); } if (cmcdConfiguration.isPlaybackRateLoggingAllowed()) { cmcdSession.setPlaybackRate(playbackRate); @@ -306,8 +403,7 @@ public final class CmcdData { CmcdStatus.Builder cmcdStatus = new CmcdStatus.Builder(); if (cmcdConfiguration.isMaximumRequestThroughputLoggingAllowed()) { - cmcdStatus.setMaximumRequestedThroughputKbps( - cmcdConfiguration.requestConfig.getRequestedMaximumThroughputKbps(bitrateKbps)); + cmcdStatus.setMaximumRequestedThroughputKbps(requestedMaximumThroughputKbps); } if (cmcdConfiguration.isBufferStarvationLoggingAllowed()) { cmcdStatus.setBufferStarvation(didRebuffer); @@ -324,8 +420,14 @@ public final class CmcdData { cmcdConfiguration.dataTransmissionMode); } - private boolean getIsInitSegment() { - return objectType != null && objectType.equals(OBJECT_TYPE_INIT_SEGMENT); + private static boolean isManifestObjectType(@Nullable @ObjectType String objectType) { + return Objects.equals(objectType, Factory.OBJECT_TYPE_MANIFEST); + } + + private static boolean isMediaObjectType(@Nullable @ObjectType String objectType) { + return Objects.equals(objectType, Factory.OBJECT_TYPE_AUDIO_ONLY) + || Objects.equals(objectType, Factory.OBJECT_TYPE_VIDEO_ONLY) + || Objects.equals(objectType, Factory.OBJECT_TYPE_MUXED_AUDIO_AND_VIDEO); } private void validateCustomDataListFormat(List customDataList) { @@ -360,7 +462,8 @@ public final class CmcdData { Factory.OBJECT_TYPE_INIT_SEGMENT, Factory.OBJECT_TYPE_AUDIO_ONLY, Factory.OBJECT_TYPE_VIDEO_ONLY, - Factory.OBJECT_TYPE_MUXED_AUDIO_AND_VIDEO + Factory.OBJECT_TYPE_MUXED_AUDIO_AND_VIDEO, + Factory.OBJECT_TYPE_MANIFEST }) @Documented @Target(TYPE_USE) @@ -528,10 +631,8 @@ public final class CmcdData { public final long objectDurationMs; /** - * The media type of the current object being requested , or {@code null} if unset. Must be one - * of the allowed object types specified by the {@link ObjectType} annotation. - * - *

If the object type being requested is unknown, then this key MUST NOT be used. + * The media type of the current object being requested. Must be one of the allowed object types + * specified by the {@link ObjectType} annotation. */ @Nullable public final @ObjectType String objectType; @@ -607,8 +708,13 @@ public final class CmcdData { */ @CanIgnoreReturnValue public Builder setBufferLengthMs(long bufferLengthMs) { - checkArgument(bufferLengthMs >= 0 || bufferLengthMs == C.TIME_UNSET); - this.bufferLengthMs = ((bufferLengthMs + 50) / 100) * 100; + if (bufferLengthMs == C.TIME_UNSET) { + this.bufferLengthMs = bufferLengthMs; + } else if (bufferLengthMs >= 0) { + this.bufferLengthMs = ((bufferLengthMs + 50) / 100) * 100; + } else { + throw new IllegalArgumentException(); + } return this; } @@ -621,10 +727,13 @@ public final class CmcdData { */ @CanIgnoreReturnValue public Builder setMeasuredThroughputInKbps(long measuredThroughputInKbps) { - checkArgument( - measuredThroughputInKbps >= 0 || measuredThroughputInKbps == C.RATE_UNSET_INT); - this.measuredThroughputInKbps = ((measuredThroughputInKbps + 50) / 100) * 100; - + if (measuredThroughputInKbps == C.RATE_UNSET_INT) { + this.measuredThroughputInKbps = measuredThroughputInKbps; + } else if (measuredThroughputInKbps >= 0) { + this.measuredThroughputInKbps = ((measuredThroughputInKbps + 50) / 100) * 100; + } else { + throw new IllegalArgumentException(); + } return this; } @@ -637,8 +746,13 @@ public final class CmcdData { */ @CanIgnoreReturnValue public Builder setDeadlineMs(long deadlineMs) { - checkArgument(deadlineMs >= 0 || deadlineMs == C.TIME_UNSET); - this.deadlineMs = ((deadlineMs + 50) / 100) * 100; + if (deadlineMs == C.TIME_UNSET) { + this.deadlineMs = deadlineMs; + } else if (deadlineMs >= 0) { + this.deadlineMs = ((deadlineMs + 50) / 100) * 100; + } else { + throw new IllegalArgumentException(); + } return this; } diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/CmcdDataTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/CmcdDataTest.java index bafe3e352b..65ff949074 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/CmcdDataTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/CmcdDataTest.java @@ -36,23 +36,48 @@ import org.junit.runner.RunWith; public class CmcdDataTest { @Test - public void createInstance_populatesCmcdHttRequestHeaders() { + public void createInstance_withInvalidFactoryState_throwsIllegalStateException() { + CmcdConfiguration cmcdConfiguration = + CmcdConfiguration.Factory.DEFAULT.createCmcdConfiguration(MediaItem.EMPTY); + ExoTrackSelection trackSelection = mock(ExoTrackSelection.class); + + assertThrows( + "Track selection must be set", + IllegalStateException.class, + () -> + new CmcdData.Factory(cmcdConfiguration, CmcdData.Factory.STREAMING_FORMAT_DASH) + .setObjectType(CmcdData.Factory.OBJECT_TYPE_INIT_SEGMENT) + .createCmcdData()); + + assertThrows( + "Buffered duration must be set", + IllegalStateException.class, + () -> + new CmcdData.Factory(cmcdConfiguration, CmcdData.Factory.STREAMING_FORMAT_DASH) + .setObjectType(CmcdData.Factory.OBJECT_TYPE_AUDIO_ONLY) + .setTrackSelection(trackSelection) + .setChunkDurationUs(100_000) + .createCmcdData()); + + assertThrows( + "Chunk duration must be set", + IllegalStateException.class, + () -> + new CmcdData.Factory(cmcdConfiguration, CmcdData.Factory.STREAMING_FORMAT_DASH) + .setObjectType(CmcdData.Factory.OBJECT_TYPE_AUDIO_ONLY) + .setTrackSelection(trackSelection) + .setBufferedDurationUs(100_000) + .createCmcdData()); + } + + @Test + public void createInstance_audioObjectType_setsCorrectHttpHeaders() { CmcdConfiguration.Factory cmcdConfigurationFactory = mediaItem -> new CmcdConfiguration( "sessionId", mediaItem.mediaId, new CmcdConfiguration.RequestConfig() { - @Override - public ImmutableListMultimap<@CmcdConfiguration.HeaderKey String, String> - getCustomData() { - return new ImmutableListMultimap.Builder() - .putAll("CMCD-Object", "key-1=1", "key-2-separated-by-multiple-hyphens=2") - .put("CMCD-Request", "key-3=\"stringValue1,stringValue2\"") - .put("CMCD-Status", "key-4=\"stringValue3=stringValue4\"") - .build(); - } - @Override public int getRequestedMaximumThroughputKbps(int throughputKbps) { return 2 * throughputKbps; @@ -69,15 +94,14 @@ public class CmcdDataTest { when(trackSelection.getLatestBitrateEstimate()).thenReturn(500_000L); DataSpec dataSpec = new DataSpec.Builder().setUri(Uri.EMPTY).build(); CmcdData cmcdData = - new CmcdData.Factory( - cmcdConfiguration, - trackSelection, - /* bufferedDurationUs= */ 1_760_000, - /* playbackRate= */ 2.0f, - /* streamingFormat= */ CmcdData.Factory.STREAMING_FORMAT_DASH, - /* isLive= */ true, - /* didRebuffer= */ true, - /* isBufferEmpty= */ false) + new CmcdData.Factory(cmcdConfiguration, CmcdData.Factory.STREAMING_FORMAT_DASH) + .setTrackSelection(trackSelection) + .setObjectType(CmcdData.Factory.OBJECT_TYPE_AUDIO_ONLY) + .setBufferedDurationUs(1_760_000) + .setPlaybackRate(2.0f) + .setIsLive(true) + .setDidRebuffer(true) + .setIsBufferEmpty(false) .setChunkDurationUs(3_000_000) .createCmcdData(); @@ -86,32 +110,23 @@ public class CmcdDataTest { assertThat(dataSpec.httpRequestHeaders) .containsExactly( "CMCD-Object", - "br=840,d=3000,key-1=1,key-2-separated-by-multiple-hyphens=2,tb=1000", + "br=840,d=3000,ot=a,tb=1000", "CMCD-Request", - "bl=1800,dl=900,key-3=\"stringValue1,stringValue2\",mtp=500,su", + "bl=1800,dl=900,mtp=500,su", "CMCD-Session", "cid=\"mediaId\",pr=2.00,sf=d,sid=\"sessionId\",st=l", "CMCD-Status", - "bs,key-4=\"stringValue3=stringValue4\",rtp=1700"); + "bs,rtp=1700"); } @Test - public void createInstance_populatesCmcdHttpQueryParameters() { + public void createInstance_audioObjectType_setsCorrectQueryParameters() { CmcdConfiguration.Factory cmcdConfigurationFactory = mediaItem -> new CmcdConfiguration( "sessionId", mediaItem.mediaId, new CmcdConfiguration.RequestConfig() { - @Override - public ImmutableListMultimap<@CmcdConfiguration.HeaderKey String, String> - getCustomData() { - return new ImmutableListMultimap.Builder() - .put("CMCD-Object", "key-1=1") - .put("CMCD-Request", "key-2=\"stringVälue1,stringVälue2\"") - .build(); - } - @Override public int getRequestedMaximumThroughputKbps(int throughputKbps) { return 2 * throughputKbps; @@ -129,32 +144,148 @@ public class CmcdDataTest { when(trackSelection.getLatestBitrateEstimate()).thenReturn(500_000L); DataSpec dataSpec = new DataSpec.Builder().setUri(Uri.EMPTY).build(); CmcdData cmcdData = - new CmcdData.Factory( - cmcdConfiguration, - trackSelection, - /* bufferedDurationUs= */ 1_760_000, - /* playbackRate= */ 2.0f, - /* streamingFormat= */ CmcdData.Factory.STREAMING_FORMAT_DASH, - /* isLive= */ true, - /* didRebuffer= */ true, - /* isBufferEmpty= */ false) + new CmcdData.Factory(cmcdConfiguration, CmcdData.Factory.STREAMING_FORMAT_DASH) + .setObjectType(CmcdData.Factory.OBJECT_TYPE_AUDIO_ONLY) + .setTrackSelection(trackSelection) + .setBufferedDurationUs(1_760_000) + .setPlaybackRate(2.0f) + .setIsLive(true) + .setDidRebuffer(true) + .setIsBufferEmpty(false) .setChunkDurationUs(3_000_000) .createCmcdData(); dataSpec = cmcdData.addToDataSpec(dataSpec); - // Confirm that the values above are URL-encoded - assertThat(dataSpec.uri.toString()).doesNotContain("ä"); - assertThat(dataSpec.uri.toString()).contains(Uri.encode("ä")); assertThat(dataSpec.uri.getQueryParameter(CmcdConfiguration.CMCD_QUERY_PARAMETER_KEY)) .isEqualTo( - "bl=1800,br=840,bs,cid=\"mediaId\",d=3000,dl=900,key-1=1," - + "key-2=\"stringVälue1,stringVälue2\",mtp=500,pr=2.00,rtp=1700,sf=d," - + "sid=\"sessionId\",st=l,su,tb=1000"); + "bl=1800,br=840,bs,cid=\"mediaId\",d=3000,dl=900,mtp=500,ot=a,pr=2.00," + + "rtp=1700,sf=d,sid=\"sessionId\",st=l,su,tb=1000"); } @Test - public void createInstance_withInvalidNonHyphenatedCustomKey_throwsIllegalStateException() { + public void createInstance_manifestObjectType_setsCorrectHttpHeaders() { + CmcdConfiguration.Factory cmcdConfigurationFactory = + mediaItem -> + new CmcdConfiguration( + "sessionId", mediaItem.mediaId, new CmcdConfiguration.RequestConfig() {}); + MediaItem mediaItem = new MediaItem.Builder().setMediaId("mediaId").build(); + CmcdConfiguration cmcdConfiguration = + cmcdConfigurationFactory.createCmcdConfiguration(mediaItem); + DataSpec dataSpec = new DataSpec.Builder().setUri(Uri.EMPTY).build(); + CmcdData cmcdData = + new CmcdData.Factory(cmcdConfiguration, CmcdData.Factory.STREAMING_FORMAT_DASH) + .setObjectType(CmcdData.Factory.OBJECT_TYPE_MANIFEST) + .createCmcdData(); + + dataSpec = cmcdData.addToDataSpec(dataSpec); + + assertThat(dataSpec.httpRequestHeaders) + .containsExactly( + "CMCD-Object", "ot=m", "CMCD-Session", "cid=\"mediaId\",sf=d,sid=\"sessionId\""); + } + + @Test + public void createInstance_manifestObjectType_setsCorrectQueryParameters() { + CmcdConfiguration.Factory cmcdConfigurationFactory = + mediaItem -> + new CmcdConfiguration( + "sessionId", + mediaItem.mediaId, + new CmcdConfiguration.RequestConfig() {}, + CmcdConfiguration.MODE_QUERY_PARAMETER); + MediaItem mediaItem = new MediaItem.Builder().setMediaId("mediaId").build(); + CmcdConfiguration cmcdConfiguration = + cmcdConfigurationFactory.createCmcdConfiguration(mediaItem); + DataSpec dataSpec = new DataSpec.Builder().setUri(Uri.EMPTY).build(); + CmcdData cmcdData = + new CmcdData.Factory(cmcdConfiguration, CmcdData.Factory.STREAMING_FORMAT_DASH) + .setObjectType(CmcdData.Factory.OBJECT_TYPE_MANIFEST) + .createCmcdData(); + + dataSpec = cmcdData.addToDataSpec(dataSpec); + + assertThat(dataSpec.uri.getQueryParameter(CmcdConfiguration.CMCD_QUERY_PARAMETER_KEY)) + .isEqualTo("cid=\"mediaId\",ot=m,sf=d,sid=\"sessionId\""); + } + + @Test + public void createInstance_unsetObjectType_setsCorrectHttpHeaders() { + CmcdConfiguration.Factory cmcdConfigurationFactory = + mediaItem -> + new CmcdConfiguration( + "sessionId", mediaItem.mediaId, new CmcdConfiguration.RequestConfig() {}); + MediaItem mediaItem = new MediaItem.Builder().setMediaId("mediaId").build(); + CmcdConfiguration cmcdConfiguration = + cmcdConfigurationFactory.createCmcdConfiguration(mediaItem); + ExoTrackSelection trackSelection = mock(ExoTrackSelection.class); + Format format = new Format.Builder().setPeakBitrate(840_000).build(); + when(trackSelection.getSelectedFormat()).thenReturn(format); + when(trackSelection.getTrackGroup()) + .thenReturn(new TrackGroup(format, new Format.Builder().setPeakBitrate(1_000_000).build())); + when(trackSelection.getLatestBitrateEstimate()).thenReturn(500_000L); + DataSpec dataSpec = new DataSpec.Builder().setUri(Uri.EMPTY).build(); + CmcdData cmcdData = + new CmcdData.Factory(cmcdConfiguration, CmcdData.Factory.STREAMING_FORMAT_DASH) + .setTrackSelection(trackSelection) + .setPlaybackRate(2.0f) + .setIsLive(true) + .setDidRebuffer(true) + .setIsBufferEmpty(false) + .createCmcdData(); + + dataSpec = cmcdData.addToDataSpec(dataSpec); + + assertThat(dataSpec.httpRequestHeaders) + .containsExactly( + "CMCD-Object", + "br=840,tb=1000", + "CMCD-Request", + "mtp=500,su", + "CMCD-Session", + "cid=\"mediaId\",pr=2.00,sf=d,sid=\"sessionId\",st=l", + "CMCD-Status", + "bs"); + } + + @Test + public void createInstance_unsetObjectType_setsCorrectQueryParameters() { + CmcdConfiguration.Factory cmcdConfigurationFactory = + mediaItem -> + new CmcdConfiguration( + "sessionId", + mediaItem.mediaId, + new CmcdConfiguration.RequestConfig() {}, + CmcdConfiguration.MODE_QUERY_PARAMETER); + MediaItem mediaItem = new MediaItem.Builder().setMediaId("mediaId").build(); + CmcdConfiguration cmcdConfiguration = + cmcdConfigurationFactory.createCmcdConfiguration(mediaItem); + ExoTrackSelection trackSelection = mock(ExoTrackSelection.class); + Format format = new Format.Builder().setPeakBitrate(840_000).build(); + when(trackSelection.getSelectedFormat()).thenReturn(format); + when(trackSelection.getTrackGroup()) + .thenReturn(new TrackGroup(format, new Format.Builder().setPeakBitrate(1_000_000).build())); + when(trackSelection.getLatestBitrateEstimate()).thenReturn(500_000L); + DataSpec dataSpec = new DataSpec.Builder().setUri(Uri.EMPTY).build(); + CmcdData cmcdData = + new CmcdData.Factory(cmcdConfiguration, CmcdData.Factory.STREAMING_FORMAT_DASH) + .setTrackSelection(trackSelection) + .setPlaybackRate(2.0f) + .setIsLive(true) + .setDidRebuffer(true) + .setIsBufferEmpty(false) + .createCmcdData(); + + dataSpec = cmcdData.addToDataSpec(dataSpec); + + assertThat(dataSpec.uri.getQueryParameter(CmcdConfiguration.CMCD_QUERY_PARAMETER_KEY)) + .isEqualTo( + "br=840,bs,cid=\"mediaId\",mtp=500,pr=2.00," + + "sf=d,sid=\"sessionId\",st=l,su,tb=1000"); + } + + @Test + public void createInstance_customData_setsCorrectHttpHeaders() { CmcdConfiguration.Factory cmcdConfigurationFactory = mediaItem -> new CmcdConfiguration( @@ -164,27 +295,94 @@ public class CmcdDataTest { @Override public ImmutableListMultimap<@CmcdConfiguration.HeaderKey String, String> getCustomData() { + return new ImmutableListMultimap.Builder() + .putAll("CMCD-Object", "key-1=1", "key-2-separated-by-multiple-hyphens=2") + .put("CMCD-Request", "key-3=\"stringValue1,stringValue2\"") + .put("CMCD-Session", "key-4=0.5") + .put("CMCD-Status", "key-5=\"stringValue3=stringValue4\"") + .build(); + } + }); + CmcdConfiguration cmcdConfiguration = + cmcdConfigurationFactory.createCmcdConfiguration(MediaItem.EMPTY); + DataSpec dataSpec = new DataSpec.Builder().setUri(Uri.EMPTY).build(); + CmcdData cmcdData = + new CmcdData.Factory(cmcdConfiguration, CmcdData.Factory.STREAMING_FORMAT_DASH) + .setObjectType(CmcdData.Factory.OBJECT_TYPE_MANIFEST) + .createCmcdData(); + + dataSpec = cmcdData.addToDataSpec(dataSpec); + + assertThat(dataSpec.httpRequestHeaders) + .containsExactly( + "CMCD-Object", + "key-1=1,key-2-separated-by-multiple-hyphens=2,ot=m", + "CMCD-Request", + "key-3=\"stringValue1,stringValue2\"", + "CMCD-Session", + "key-4=0.5,sf=d", + "CMCD-Status", + "key-5=\"stringValue3=stringValue4\""); + } + + @Test + public void createInstance_customData_setsCorrectQueryParameters() { + CmcdConfiguration.Factory cmcdConfigurationFactory = + mediaItem -> + new CmcdConfiguration( + null, + null, + new CmcdConfiguration.RequestConfig() { + @Override + public ImmutableListMultimap<@CmcdConfiguration.HeaderKey String, String> + getCustomData() { + return new ImmutableListMultimap.Builder() + .put("CMCD-Object", "key-1=1") + .put("CMCD-Request", "key-2=\"stringVälue1,stringVälue2\"") + .build(); + } + }, + CmcdConfiguration.MODE_QUERY_PARAMETER); + CmcdConfiguration cmcdConfiguration = + cmcdConfigurationFactory.createCmcdConfiguration(MediaItem.EMPTY); + DataSpec dataSpec = new DataSpec.Builder().setUri(Uri.EMPTY).build(); + CmcdData cmcdData = + new CmcdData.Factory(cmcdConfiguration, CmcdData.Factory.STREAMING_FORMAT_DASH) + .setObjectType(CmcdData.Factory.OBJECT_TYPE_MANIFEST) + .createCmcdData(); + + dataSpec = cmcdData.addToDataSpec(dataSpec); + + // Confirm that the values above are URL-encoded + assertThat(dataSpec.uri.toString()).doesNotContain("ä"); + assertThat(dataSpec.uri.toString()).contains(Uri.encode("ä")); + assertThat(dataSpec.uri.getQueryParameter(CmcdConfiguration.CMCD_QUERY_PARAMETER_KEY)) + .isEqualTo("key-1=1,key-2=\"stringVälue1,stringVälue2\",ot=m,sf=d"); + } + + @Test + public void createInstance_invalidCustomDataKey_throwsException() { + CmcdConfiguration.Factory cmcdConfigurationFactory = + mediaItem -> + new CmcdConfiguration( + null, + null, + new CmcdConfiguration.RequestConfig() { + @Override + public ImmutableListMultimap<@CmcdConfiguration.HeaderKey String, String> + getCustomData() { + // Invalid non-hyphenated key1 return ImmutableListMultimap.of("CMCD-Object", "key1=1"); } }); - MediaItem mediaItem = new MediaItem.Builder().setMediaId("mediaId").build(); CmcdConfiguration cmcdConfiguration = - cmcdConfigurationFactory.createCmcdConfiguration(mediaItem); - ExoTrackSelection trackSelection = mock(ExoTrackSelection.class); - when(trackSelection.getSelectedFormat()).thenReturn(new Format.Builder().build()); + cmcdConfigurationFactory.createCmcdConfiguration(MediaItem.EMPTY); assertThrows( IllegalStateException.class, () -> - new CmcdData.Factory( - cmcdConfiguration, - trackSelection, - /* bufferedDurationUs= */ 0, - /* playbackRate= */ 1.0f, - /* streamingFormat= */ CmcdData.Factory.STREAMING_FORMAT_DASH, - /* isLive= */ true, - /* didRebuffer= */ true, - /* isBufferEmpty= */ false) + new CmcdData.Factory(cmcdConfiguration, CmcdData.Factory.STREAMING_FORMAT_DASH) + .setObjectType(CmcdData.Factory.OBJECT_TYPE_MANIFEST) .createCmcdData()); } } diff --git a/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DashMediaSource.java b/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DashMediaSource.java index 0ef7df98db..d2d67c80c5 100644 --- a/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DashMediaSource.java +++ b/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DashMediaSource.java @@ -43,6 +43,7 @@ import androidx.media3.common.util.Log; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import androidx.media3.datasource.DataSource; +import androidx.media3.datasource.DataSpec; import androidx.media3.datasource.TransferListener; import androidx.media3.exoplayer.dash.PlayerEmsgHandler.PlayerEmsgCallback; import androidx.media3.exoplayer.dash.manifest.AdaptationSet; @@ -69,6 +70,7 @@ import androidx.media3.exoplayer.source.MediaSourceFactory; import androidx.media3.exoplayer.source.SequenceableLoader; import androidx.media3.exoplayer.upstream.Allocator; import androidx.media3.exoplayer.upstream.CmcdConfiguration; +import androidx.media3.exoplayer.upstream.CmcdData; import androidx.media3.exoplayer.upstream.DefaultLoadErrorHandlingPolicy; import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy; import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy.LoadErrorInfo; @@ -1099,8 +1101,19 @@ public final class DashMediaSource extends BaseMediaSource { manifestUri = this.manifestUri; } manifestLoadPending = false; + DataSpec dataSpec = + new DataSpec.Builder().setUri(manifestUri).setFlags(DataSpec.FLAG_ALLOW_GZIP).build(); + if (cmcdConfiguration != null) { + CmcdData.Factory cmcdDataFactory = + new CmcdData.Factory(cmcdConfiguration, CmcdData.Factory.STREAMING_FORMAT_DASH) + .setObjectType(CmcdData.Factory.OBJECT_TYPE_MANIFEST); + if (manifest != null) { + cmcdDataFactory.setIsLive(manifest.dynamic); + } + cmcdDataFactory.createCmcdData().addToDataSpec(dataSpec); + } startLoading( - new ParsingLoadable<>(dataSource, manifestUri, C.DATA_TYPE_MANIFEST, manifestParser), + new ParsingLoadable<>(dataSource, dataSpec, C.DATA_TYPE_MANIFEST, manifestParser), manifestCallback, loadErrorHandlingPolicy.getMinimumLoadableRetryCount(C.DATA_TYPE_MANIFEST)); } diff --git a/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DefaultDashChunkSource.java b/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DefaultDashChunkSource.java index e94de25ea3..b651ce73fe 100644 --- a/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DefaultDashChunkSource.java +++ b/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DefaultDashChunkSource.java @@ -421,15 +421,13 @@ public class DefaultDashChunkSource implements DashChunkSource { CmcdData.Factory cmcdDataFactory = cmcdConfiguration == null ? null - : new CmcdData.Factory( - cmcdConfiguration, - trackSelection, - max(0, bufferedDurationUs), - /* playbackRate= */ loadingInfo.playbackSpeed, - /* streamingFormat= */ CmcdData.Factory.STREAMING_FORMAT_DASH, - /* isLive= */ manifest.dynamic, - /* didRebuffer= */ loadingInfo.rebufferedSince(lastChunkRequestRealtimeMs), - /* isBufferEmpty= */ queue.isEmpty()); + : new CmcdData.Factory(cmcdConfiguration, CmcdData.Factory.STREAMING_FORMAT_DASH) + .setTrackSelection(trackSelection) + .setBufferedDurationUs(max(0, bufferedDurationUs)) + .setPlaybackRate(loadingInfo.playbackSpeed) + .setIsLive(manifest.dynamic) + .setDidRebuffer(loadingInfo.rebufferedSince(lastChunkRequestRealtimeMs)) + .setIsBufferEmpty(queue.isEmpty()); lastChunkRequestRealtimeMs = SystemClock.elapsedRealtime(); RepresentationHolder representationHolder = updateSelectedBaseUrl(selectedTrackIndex); @@ -715,7 +713,7 @@ public class DefaultDashChunkSource implements DashChunkSource { * indexUri} is not {@code null}. * @param indexUri The URI pointing to index data. Can be {@code null} if {@code * initializationUri} is not {@code null}. - * @param cmcdDataFactory The {@link CmcdData.Factory} for generating CMCD data. + * @param cmcdDataFactory The {@link CmcdData.Factory} for generating {@link CmcdData}. */ @RequiresNonNull("#1.chunkExtractor") protected Chunk newInitializationChunk( diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsChunkSource.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsChunkSource.java index 6867a3291e..68a5374fb9 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsChunkSource.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsChunkSource.java @@ -507,20 +507,17 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Nullable CmcdData.Factory cmcdDataFactory = null; if (cmcdConfiguration != null) { cmcdDataFactory = - new CmcdData.Factory( - cmcdConfiguration, - trackSelection, - max(0, bufferedDurationUs), - /* playbackRate= */ loadingInfo.playbackSpeed, - /* streamingFormat= */ CmcdData.Factory.STREAMING_FORMAT_HLS, - /* isLive= */ !playlist.hasEndTag, - /* didRebuffer= */ loadingInfo.rebufferedSince(lastChunkRequestRealtimeMs), - /* isBufferEmpty= */ queue.isEmpty()) + new CmcdData.Factory(cmcdConfiguration, CmcdData.Factory.STREAMING_FORMAT_HLS) + .setTrackSelection(trackSelection) + .setBufferedDurationUs(max(0, bufferedDurationUs)) + .setPlaybackRate(loadingInfo.playbackSpeed) + .setIsLive(!playlist.hasEndTag) + .setDidRebuffer(loadingInfo.rebufferedSince(lastChunkRequestRealtimeMs)) + .setIsBufferEmpty(queue.isEmpty()) .setObjectType( getIsMuxedAudioAndVideo() ? CmcdData.Factory.OBJECT_TYPE_MUXED_AUDIO_AND_VIDEO : CmcdData.Factory.getObjectType(trackSelection)); - long nextMediaSequence = segmentBaseHolder.partIndex == C.INDEX_UNSET ? segmentBaseHolder.mediaSequence + 1 diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaSource.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaSource.java index 7985d7211a..df972d81a6 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaSource.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaSource.java @@ -417,7 +417,10 @@ public final class HlsMediaSource extends BaseMediaSource drmSessionManagerProvider.get(mediaItem), loadErrorHandlingPolicy, playlistTrackerFactory.createTracker( - hlsDataSourceFactory, loadErrorHandlingPolicy, playlistParserFactory), + hlsDataSourceFactory, + loadErrorHandlingPolicy, + playlistParserFactory, + cmcdConfiguration), elapsedRealTimeOffsetMs, allowChunklessPreparation, metadataType, diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/DefaultHlsPlaylistTracker.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/DefaultHlsPlaylistTracker.java index 505841af42..9254177423 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/DefaultHlsPlaylistTracker.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/DefaultHlsPlaylistTracker.java @@ -29,6 +29,7 @@ import androidx.media3.common.util.Assertions; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import androidx.media3.datasource.DataSource; +import androidx.media3.datasource.DataSpec; import androidx.media3.datasource.HttpDataSource; import androidx.media3.exoplayer.hls.HlsDataSourceFactory; import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist.Part; @@ -38,6 +39,8 @@ import androidx.media3.exoplayer.hls.playlist.HlsMultivariantPlaylist.Variant; import androidx.media3.exoplayer.source.LoadEventInfo; import androidx.media3.exoplayer.source.MediaLoadData; import androidx.media3.exoplayer.source.MediaSourceEventListener.EventDispatcher; +import androidx.media3.exoplayer.upstream.CmcdConfiguration; +import androidx.media3.exoplayer.upstream.CmcdData; import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy; import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy.LoadErrorInfo; import androidx.media3.exoplayer.upstream.Loader; @@ -69,6 +72,7 @@ public final class DefaultHlsPlaylistTracker private final HashMap playlistBundles; private final CopyOnWriteArrayList listeners; private final double playlistStuckTargetDurationCoefficient; + @Nullable private final CmcdConfiguration cmcdConfiguration; @Nullable private EventDispatcher eventDispatcher; @Nullable private Loader initialPlaylistLoader; @@ -86,15 +90,18 @@ public final class DefaultHlsPlaylistTracker * @param dataSourceFactory A factory for {@link DataSource} instances. * @param loadErrorHandlingPolicy The {@link LoadErrorHandlingPolicy}. * @param playlistParserFactory An {@link HlsPlaylistParserFactory}. + * @param cmcdConfiguration The {@link CmcdConfiguration}. */ public DefaultHlsPlaylistTracker( HlsDataSourceFactory dataSourceFactory, LoadErrorHandlingPolicy loadErrorHandlingPolicy, - HlsPlaylistParserFactory playlistParserFactory) { + HlsPlaylistParserFactory playlistParserFactory, + @Nullable CmcdConfiguration cmcdConfiguration) { this( dataSourceFactory, loadErrorHandlingPolicy, playlistParserFactory, + cmcdConfiguration, DEFAULT_PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT); } @@ -104,6 +111,7 @@ public final class DefaultHlsPlaylistTracker * @param dataSourceFactory A factory for {@link DataSource} instances. * @param loadErrorHandlingPolicy The {@link LoadErrorHandlingPolicy}. * @param playlistParserFactory An {@link HlsPlaylistParserFactory}. + * @param cmcdConfiguration The {@link CmcdConfiguration}. * @param playlistStuckTargetDurationCoefficient A coefficient to apply to the target duration of * media playlists in order to determine that a non-changing playlist is stuck. Once a * playlist is deemed stuck, a {@link PlaylistStuckException} is thrown via {@link @@ -113,10 +121,12 @@ public final class DefaultHlsPlaylistTracker HlsDataSourceFactory dataSourceFactory, LoadErrorHandlingPolicy loadErrorHandlingPolicy, HlsPlaylistParserFactory playlistParserFactory, + @Nullable CmcdConfiguration cmcdConfiguration, double playlistStuckTargetDurationCoefficient) { this.dataSourceFactory = dataSourceFactory; this.playlistParserFactory = playlistParserFactory; this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; + this.cmcdConfiguration = cmcdConfiguration; this.playlistStuckTargetDurationCoefficient = playlistStuckTargetDurationCoefficient; listeners = new CopyOnWriteArrayList<>(); playlistBundles = new HashMap<>(); @@ -133,10 +143,22 @@ public final class DefaultHlsPlaylistTracker this.playlistRefreshHandler = Util.createHandlerForCurrentLooper(); this.eventDispatcher = eventDispatcher; this.primaryPlaylistListener = primaryPlaylistListener; + DataSpec dataSpec = + new DataSpec.Builder() + .setUri(initialPlaylistUri) + .setFlags(DataSpec.FLAG_ALLOW_GZIP) + .build(); + if (cmcdConfiguration != null) { + CmcdData cmcdData = + new CmcdData.Factory(cmcdConfiguration, CmcdData.Factory.STREAMING_FORMAT_HLS) + .setObjectType(CmcdData.Factory.OBJECT_TYPE_MANIFEST) + .createCmcdData(); + cmcdData.addToDataSpec(dataSpec); + } ParsingLoadable multivariantPlaylistLoadable = new ParsingLoadable<>( dataSourceFactory.createDataSource(C.DATA_TYPE_MANIFEST), - initialPlaylistUri, + dataSpec, C.DATA_TYPE_MANIFEST, playlistParserFactory.createPlaylistParser()); Assertions.checkState(initialPlaylistLoader == null); @@ -762,12 +784,23 @@ public final class DefaultHlsPlaylistTracker private void loadPlaylistImmediately(Uri playlistRequestUri) { ParsingLoadable.Parser mediaPlaylistParser = playlistParserFactory.createPlaylistParser(multivariantPlaylist, playlistSnapshot); + DataSpec dataSpec = + new DataSpec.Builder() + .setUri(playlistRequestUri) + .setFlags(DataSpec.FLAG_ALLOW_GZIP) + .build(); + if (cmcdConfiguration != null) { + CmcdData.Factory cmcdDataFactory = + new CmcdData.Factory(cmcdConfiguration, CmcdData.Factory.STREAMING_FORMAT_HLS) + .setObjectType(CmcdData.Factory.OBJECT_TYPE_MANIFEST); + if (primaryMediaPlaylistSnapshot != null) { + cmcdDataFactory.setIsLive(!primaryMediaPlaylistSnapshot.hasEndTag); + } + cmcdDataFactory.createCmcdData().addToDataSpec(dataSpec); + } ParsingLoadable mediaPlaylistLoadable = new ParsingLoadable<>( - mediaPlaylistDataSource, - playlistRequestUri, - C.DATA_TYPE_MANIFEST, - mediaPlaylistParser); + mediaPlaylistDataSource, dataSpec, C.DATA_TYPE_MANIFEST, mediaPlaylistParser); mediaPlaylistLoader.startLoading( mediaPlaylistLoadable, /* callback= */ this, diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsPlaylistTracker.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsPlaylistTracker.java index d08874232a..b42128bae0 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsPlaylistTracker.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsPlaylistTracker.java @@ -21,6 +21,7 @@ import androidx.media3.common.C; import androidx.media3.common.util.UnstableApi; import androidx.media3.exoplayer.hls.HlsDataSourceFactory; import androidx.media3.exoplayer.source.MediaSourceEventListener.EventDispatcher; +import androidx.media3.exoplayer.upstream.CmcdConfiguration; import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy; import java.io.IOException; @@ -48,11 +49,13 @@ public interface HlsPlaylistTracker { * @param dataSourceFactory The {@link HlsDataSourceFactory} to use for playlist loading. * @param loadErrorHandlingPolicy The {@link LoadErrorHandlingPolicy} for playlist load errors. * @param playlistParserFactory The {@link HlsPlaylistParserFactory} for playlist parsing. + * @param cmcdConfiguration The {@link CmcdConfiguration} to use for playlist loading. */ HlsPlaylistTracker createTracker( HlsDataSourceFactory dataSourceFactory, LoadErrorHandlingPolicy loadErrorHandlingPolicy, - HlsPlaylistParserFactory playlistParserFactory); + HlsPlaylistParserFactory playlistParserFactory, + @Nullable CmcdConfiguration cmcdConfiguration); } /** Listener for primary playlist changes. */ diff --git a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/playlist/DefaultHlsPlaylistTrackerTest.java b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/playlist/DefaultHlsPlaylistTrackerTest.java index 23000b89c7..67420ec0aa 100644 --- a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/playlist/DefaultHlsPlaylistTrackerTest.java +++ b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/playlist/DefaultHlsPlaylistTrackerTest.java @@ -404,7 +404,8 @@ public class DefaultHlsPlaylistTrackerTest { new DefaultHlsPlaylistTracker( dataType -> new DefaultHttpDataSource.Factory().createDataSource(), new DefaultLoadErrorHandlingPolicy(), - new DefaultHlsPlaylistParserFactory()); + new DefaultHlsPlaylistParserFactory(), + /* cmcdConfiguration= */ null); AtomicInteger playlistChangedCounter = new AtomicInteger(); AtomicReference audioPlaylistRefreshExceptionRef = new AtomicReference<>(); defaultHlsPlaylistTracker.addListener( @@ -486,7 +487,8 @@ public class DefaultHlsPlaylistTrackerTest { new DefaultHlsPlaylistTracker( dataType -> new DefaultHttpDataSource.Factory().createDataSource(), new DefaultLoadErrorHandlingPolicy(), - new DefaultHlsPlaylistParserFactory()); + new DefaultHlsPlaylistParserFactory(), + /* cmcdConfiguration= */ null); List mediaPlaylists = new ArrayList<>(); AtomicInteger playlistCounter = new AtomicInteger(); AtomicReference primaryPlaylistChangeExceptionRef = new AtomicReference<>(); @@ -562,7 +564,8 @@ public class DefaultHlsPlaylistTrackerTest { new DefaultHlsPlaylistTracker( dataType -> new DefaultHttpDataSource.Factory().createDataSource(), new DefaultLoadErrorHandlingPolicy(), - new DefaultHlsPlaylistParserFactory()); + new DefaultHlsPlaylistParserFactory(), + /* cmcdConfiguration= */ null); List mediaPlaylists = new ArrayList<>(); AtomicInteger playlistCounter = new AtomicInteger(); AtomicReference playlistRefreshExceptionRef = new AtomicReference<>(); @@ -674,7 +677,8 @@ public class DefaultHlsPlaylistTrackerTest { new DefaultHlsPlaylistTracker( dataType -> dataSourceFactory.createDataSource(), new DefaultLoadErrorHandlingPolicy(), - new DefaultHlsPlaylistParserFactory()); + new DefaultHlsPlaylistParserFactory(), + /* cmcdConfiguration= */ null); List mediaPlaylists = new ArrayList<>(); AtomicInteger playlistCounter = new AtomicInteger(); diff --git a/libraries/exoplayer_smoothstreaming/src/main/java/androidx/media3/exoplayer/smoothstreaming/DefaultSsChunkSource.java b/libraries/exoplayer_smoothstreaming/src/main/java/androidx/media3/exoplayer/smoothstreaming/DefaultSsChunkSource.java index 311afb3c7b..3d0b9d51ca 100644 --- a/libraries/exoplayer_smoothstreaming/src/main/java/androidx/media3/exoplayer/smoothstreaming/DefaultSsChunkSource.java +++ b/libraries/exoplayer_smoothstreaming/src/main/java/androidx/media3/exoplayer/smoothstreaming/DefaultSsChunkSource.java @@ -360,15 +360,13 @@ public class DefaultSsChunkSource implements SsChunkSource { @Nullable CmcdData.Factory cmcdDataFactory = null; if (cmcdConfiguration != null) { cmcdDataFactory = - new CmcdData.Factory( - cmcdConfiguration, - trackSelection, - max(0, bufferedDurationUs), - /* playbackRate= */ loadingInfo.playbackSpeed, - /* streamingFormat= */ CmcdData.Factory.STREAMING_FORMAT_SS, - /* isLive= */ manifest.isLive, - /* didRebuffer= */ loadingInfo.rebufferedSince(lastChunkRequestRealtimeMs), - /* isBufferEmpty= */ queue.isEmpty()) + new CmcdData.Factory(cmcdConfiguration, CmcdData.Factory.STREAMING_FORMAT_SS) + .setTrackSelection(trackSelection) + .setBufferedDurationUs(max(0, bufferedDurationUs)) + .setPlaybackRate(loadingInfo.playbackSpeed) + .setIsLive(manifest.isLive) + .setDidRebuffer(loadingInfo.rebufferedSince(lastChunkRequestRealtimeMs)) + .setIsBufferEmpty(queue.isEmpty()) .setChunkDurationUs(chunkEndTimeUs - chunkStartTimeUs) .setObjectType(CmcdData.Factory.getObjectType(trackSelection)); diff --git a/libraries/exoplayer_smoothstreaming/src/main/java/androidx/media3/exoplayer/smoothstreaming/SsMediaSource.java b/libraries/exoplayer_smoothstreaming/src/main/java/androidx/media3/exoplayer/smoothstreaming/SsMediaSource.java index e402753125..a2dfd4b783 100644 --- a/libraries/exoplayer_smoothstreaming/src/main/java/androidx/media3/exoplayer/smoothstreaming/SsMediaSource.java +++ b/libraries/exoplayer_smoothstreaming/src/main/java/androidx/media3/exoplayer/smoothstreaming/SsMediaSource.java @@ -35,6 +35,7 @@ import androidx.media3.common.util.Assertions; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import androidx.media3.datasource.DataSource; +import androidx.media3.datasource.DataSpec; import androidx.media3.datasource.TransferListener; import androidx.media3.exoplayer.drm.DefaultDrmSessionManagerProvider; import androidx.media3.exoplayer.drm.DrmSessionEventListener; @@ -58,6 +59,7 @@ import androidx.media3.exoplayer.source.SequenceableLoader; import androidx.media3.exoplayer.source.SinglePeriodTimeline; import androidx.media3.exoplayer.upstream.Allocator; import androidx.media3.exoplayer.upstream.CmcdConfiguration; +import androidx.media3.exoplayer.upstream.CmcdData; import androidx.media3.exoplayer.upstream.DefaultLoadErrorHandlingPolicy; import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy; import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy.LoadErrorInfo; @@ -676,9 +678,19 @@ public final class SsMediaSource extends BaseMediaSource if (manifestLoader.hasFatalError()) { return; } + DataSpec dataSpec = + new DataSpec.Builder().setUri(manifestUri).setFlags(DataSpec.FLAG_ALLOW_GZIP).build(); + if (cmcdConfiguration != null) { + CmcdData.Factory cmcdDataFactory = + new CmcdData.Factory(cmcdConfiguration, CmcdData.Factory.STREAMING_FORMAT_SS) + .setObjectType(CmcdData.Factory.OBJECT_TYPE_MANIFEST); + if (manifest != null) { + cmcdDataFactory.setIsLive(manifest.isLive); + } + cmcdDataFactory.createCmcdData().addToDataSpec(dataSpec); + } ParsingLoadable loadable = - new ParsingLoadable<>( - manifestDataSource, manifestUri, C.DATA_TYPE_MANIFEST, manifestParser); + new ParsingLoadable<>(manifestDataSource, dataSpec, C.DATA_TYPE_MANIFEST, manifestParser); manifestLoader.startLoading( loadable, this, loadErrorHandlingPolicy.getMinimumLoadableRetryCount(loadable.type)); }