From 1b2a2fcde00c5bd05573e777c4e8a2bf60db7137 Mon Sep 17 00:00:00 2001 From: rohks Date: Tue, 18 Jul 2023 11:24:34 +0100 Subject: [PATCH] Add fields top bitrate(tb) and object duration(d) Added these CMCD-Object fields to Common Media Client Data (CMCD) logging. #minor-release PiperOrigin-RevId: 548950296 --- RELEASENOTES.md | 2 + .../exoplayer/upstream/CmcdConfiguration.java | 22 ++++- .../media3/exoplayer/upstream/CmcdLog.java | 81 ++++++++++++++++++- .../exoplayer/upstream/CmcdLogTest.java | 10 ++- .../dash/DefaultDashChunkSource.java | 11 ++- .../dash/DefaultDashChunkSourceTest.java | 6 +- .../media3/exoplayer/hls/HlsChunkSource.java | 8 ++ .../exoplayer/hls/HlsChunkSourceTest.java | 6 +- .../smoothstreaming/DefaultSsChunkSource.java | 7 ++ .../DefaultSsChunkSourceTest.java | 6 +- 10 files changed, 143 insertions(+), 16 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 8b444b19c4..f9ce2de3a1 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -34,6 +34,8 @@ (([#33](https://github.com/androidx/media/issues/33)),([#9978](https://github.com/google/ExoPlayer/issues/9978))). * Add fields streaming format (sf), stream type (st) and version (v) to Common Media Client Data (CMCD) logging. + * Add fields top birate (tb) and object duration (d) to Common Media + Client Data (CMCD) logging. * Transformer: * Parse EXIF rotation data for image inputs. * Remove `TransformationRequest.HdrMode` annotation type and its diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/CmcdConfiguration.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/CmcdConfiguration.java index ad6d714481..b3c7f27d48 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/CmcdConfiguration.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/CmcdConfiguration.java @@ -63,7 +63,9 @@ public final class CmcdConfiguration { KEY_MAXIMUM_REQUESTED_BITRATE, KEY_STREAMING_FORMAT, KEY_STREAM_TYPE, - KEY_VERSION + KEY_VERSION, + KEY_TOP_BITRATE, + KEY_OBJECT_DURATION }) @Documented @Target(TYPE_USE) @@ -84,6 +86,8 @@ public final class CmcdConfiguration { public static final String KEY_STREAMING_FORMAT = "sf"; public static final String KEY_STREAM_TYPE = "st"; public static final String KEY_VERSION = "v"; + public static final String KEY_TOP_BITRATE = "tb"; + public static final String KEY_OBJECT_DURATION = "d"; /** * Factory for {@link CmcdConfiguration} instances. @@ -261,4 +265,20 @@ public final class CmcdConfiguration { public boolean isStreamTypeLoggingAllowed() { return requestConfig.isKeyAllowed(KEY_STREAM_TYPE); } + + /** + * Returns whether logging top bitrate is allowed based on the {@linkplain RequestConfig request + * configuration}. + */ + public boolean isTopBitrateLoggingAllowed() { + return requestConfig.isKeyAllowed(KEY_TOP_BITRATE); + } + + /** + * Returns whether logging object duration is allowed based on the {@linkplain RequestConfig + * request configuration}. + */ + public boolean isObjectDurationLoggingAllowed() { + return requestConfig.isKeyAllowed(KEY_OBJECT_DURATION); + } } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/CmcdLog.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/CmcdLog.java index 228f640325..52cbdf29e7 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/CmcdLog.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/CmcdLog.java @@ -16,12 +16,14 @@ package androidx.media3.exoplayer.upstream; import static androidx.media3.common.util.Assertions.checkArgument; +import static java.lang.Math.max; import static java.lang.annotation.ElementType.TYPE_USE; import android.text.TextUtils; import androidx.annotation.Nullable; import androidx.annotation.StringDef; import androidx.media3.common.C; +import androidx.media3.common.TrackGroup; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import androidx.media3.exoplayer.trackselection.ExoTrackSelection; @@ -79,6 +81,7 @@ public final class CmcdLog { * @param trackSelection The {@linkplain ExoTrackSelection track selection}. * @param bufferedDurationUs The duration of media currently buffered from the current playback * position, in microseconds. + * @param chunkDurationUs The duration of current media chunk being requested, in microseconds. * @param streamingFormat The streaming format of the media content. Must be one of the allowed * streaming formats specified by the {@link StreamingFormat} annotation. * @param isLive {@code true} if the media content is being streamed live, {@code false} @@ -88,6 +91,7 @@ public final class CmcdLog { CmcdConfiguration cmcdConfiguration, ExoTrackSelection trackSelection, long bufferedDurationUs, + long chunkDurationUs, @StreamingFormat String streamingFormat, boolean isLive) { ImmutableMap<@CmcdConfiguration.HeaderKey String, String> customData = @@ -100,6 +104,17 @@ public final class CmcdLog { 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(topBitrate / 1000); + } + if (cmcdConfiguration.isObjectDurationLoggingAllowed()) { + cmcdObject.setObjectDurationMs(chunkDurationUs / 1000); + } CmcdLog.CmcdRequest.Builder cmcdRequest = new CmcdLog.CmcdRequest.Builder() @@ -167,11 +182,15 @@ public final class CmcdLog { /** Builder for {@link CmcdObject} instances. */ public static final class Builder { private int bitrateKbps; + private int topBitrateKbps; + private long objectDurationMs; @Nullable private String customData; /** Creates a new instance with default values. */ public Builder() { this.bitrateKbps = C.RATE_UNSET_INT; + this.topBitrateKbps = C.RATE_UNSET_INT; + this.objectDurationMs = C.TIME_UNSET; } /** Sets the {@link CmcdObject#bitrateKbps}. The default value is {@link C#RATE_UNSET_INT}. */ @@ -181,6 +200,28 @@ public final class CmcdLog { return this; } + /** + * Sets the {@link CmcdObject#topBitrateKbps}. The default value is {@link C#RATE_UNSET_INT}. + */ + @CanIgnoreReturnValue + public Builder setTopBitrateKbps(int topBitrateKbps) { + this.topBitrateKbps = topBitrateKbps; + return this; + } + + /** + * Sets the {@link CmcdObject#objectDurationMs}. The default value is {@link C#TIME_UNSET}. + * + * @throws IllegalArgumentException If {@code objectDurationMs} is not equal to {@link + * C#TIME_UNSET} and is non-positive. + */ + @CanIgnoreReturnValue + public Builder setObjectDurationMs(long objectDurationMs) { + checkArgument(objectDurationMs == C.TIME_UNSET || objectDurationMs >= 0); + this.objectDurationMs = objectDurationMs; + return this; + } + /** Sets the {@link CmcdObject#customData}. The default value is {@code null}. */ @CanIgnoreReturnValue public Builder setCustomData(@Nullable String customData) { @@ -203,6 +244,21 @@ public final class CmcdLog { */ public final int bitrateKbps; + /** + * The highest bitrate rendition, in kbps, in the manifest or playlist that the client is + * allowed to play, given current codec, licensing and sizing constraints. If unset, it is + * represented by the value {@link C#RATE_UNSET_INT}. + */ + public final int topBitrateKbps; + + /** + * The playback duration in milliseconds of the object being requested, or {@link C#TIME_UNSET} + * if unset. If a partial segment is being requested, then this value MUST indicate the playback + * duration of that part and not that of its parent segment. This value can be an approximation + * of the estimated duration if the explicit value is not known. + */ + public final long objectDurationMs; + /** * Custom data where the values of the keys vary with the object being requested, or {@code * null} if unset. @@ -214,6 +270,8 @@ public final class CmcdLog { private CmcdObject(Builder builder) { this.bitrateKbps = builder.bitrateKbps; + this.topBitrateKbps = builder.topBitrateKbps; + this.objectDurationMs = builder.objectDurationMs; this.customData = builder.customData; } @@ -230,6 +288,15 @@ public final class CmcdLog { headerValue.append( Util.formatInvariant("%s=%d,", CmcdConfiguration.KEY_BITRATE, bitrateKbps)); } + if (topBitrateKbps != C.RATE_UNSET_INT) { + headerValue.append( + Util.formatInvariant("%s=%d,", CmcdConfiguration.KEY_TOP_BITRATE, topBitrateKbps)); + } + if (objectDurationMs != C.TIME_UNSET) { + headerValue.append( + Util.formatInvariant( + "%s=%d,", CmcdConfiguration.KEY_OBJECT_DURATION, objectDurationMs)); + } if (!TextUtils.isEmpty(customData)) { headerValue.append(Util.formatInvariant("%s,", customData)); } @@ -259,6 +326,9 @@ public final class CmcdLog { /** * Sets the {@link CmcdRequest#bufferLengthMs}. Rounded to nearest 100 ms. The default value * is {@link C#TIME_UNSET}. + * + * @throws IllegalArgumentException If {@code bufferLengthMs} is not equal to {@link + * C#TIME_UNSET} and is non-positive. */ @CanIgnoreReturnValue public Builder setBufferLengthMs(long bufferLengthMs) { @@ -270,7 +340,7 @@ public final class CmcdLog { /** Sets the {@link CmcdRequest#customData}. The default value is {@code null}. */ @CanIgnoreReturnValue - public CmcdRequest.Builder setCustomData(@Nullable String customData) { + public Builder setCustomData(@Nullable String customData) { this.customData = customData; return this; } @@ -344,6 +414,9 @@ public final class CmcdLog { /** * Sets the {@link CmcdSession#contentId}. Maximum length allowed is 64 characters. The * default value is {@code null}. + * + * @throws IllegalArgumentException If {@code contentId} is null or its length exceeds {@link + * CmcdConfiguration#MAX_ID_LENGTH}. */ @CanIgnoreReturnValue public Builder setContentId(@Nullable String contentId) { @@ -355,6 +428,9 @@ public final class CmcdLog { /** * Sets the {@link CmcdSession#sessionId}. Maximum length allowed is 64 characters. The * default value is {@code null}. + * + * @throws IllegalArgumentException If {@code sessionId} is null or its length exceeds {@link + * CmcdConfiguration#MAX_ID_LENGTH}. */ @CanIgnoreReturnValue public Builder setSessionId(@Nullable String sessionId) { @@ -502,6 +578,9 @@ public final class CmcdLog { /** * Sets the {@link CmcdStatus#maximumRequestedThroughputKbps}. Rounded to nearest 100 kbps. * The default value is {@link C#RATE_UNSET_INT}. + * + * @throws IllegalArgumentException If {@code maximumRequestedThroughputKbps} is not equal to + * {@link C#RATE_UNSET_INT} and is non-positive. */ @CanIgnoreReturnValue public Builder setMaximumRequestedThroughputKbps(int maximumRequestedThroughputKbps) { diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/CmcdLogTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/CmcdLogTest.java index b9a193137c..9de7bf660c 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/CmcdLogTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/CmcdLogTest.java @@ -21,6 +21,7 @@ import static org.mockito.Mockito.when; import androidx.media3.common.Format; import androidx.media3.common.MediaItem; +import androidx.media3.common.TrackGroup; import androidx.media3.exoplayer.trackselection.ExoTrackSelection; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.common.collect.ImmutableMap; @@ -56,13 +57,16 @@ public class CmcdLogTest { CmcdConfiguration cmcdConfiguration = cmcdConfigurationFactory.createCmcdConfiguration(mediaItem); ExoTrackSelection trackSelection = mock(ExoTrackSelection.class); - when(trackSelection.getSelectedFormat()) - .thenReturn(new Format.Builder().setPeakBitrate(840_000).build()); + 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())); CmcdLog cmcdLog = CmcdLog.createInstance( cmcdConfiguration, trackSelection, /* bufferedDurationUs= */ 1_760_000, + /* chunkDurationUs= */ 3_000_000, CmcdLog.STREAMING_FORMAT_DASH, true); @@ -72,7 +76,7 @@ public class CmcdLogTest { assertThat(requestHeaders) .containsExactly( "CMCD-Object", - "br=840,key1=value1", + "br=840,tb=1000,d=3000,key1=value1", "CMCD-Request", "bl=1800,key2=\"stringValue\"", "CMCD-Session", 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 2284c3c53e..71e7bb2a65 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 @@ -370,6 +370,13 @@ public class DefaultDashChunkSource implements DashChunkSource { trackSelection.updateSelectedTrack( playbackPositionUs, bufferedDurationUs, availableLiveDurationUs, queue, chunkIterators); + int selectedTrackIndex = trackSelection.getSelectedIndex(); + long chunkDurationUs = 0; + if (selectedTrackIndex < chunkIterators.length && chunkIterators[selectedTrackIndex].next()) { + chunkDurationUs = + chunkIterators[selectedTrackIndex].getChunkEndTimeUs() + - chunkIterators[selectedTrackIndex].getChunkStartTimeUs(); + } @Nullable CmcdLog cmcdLog = cmcdConfiguration == null @@ -378,11 +385,11 @@ public class DefaultDashChunkSource implements DashChunkSource { cmcdConfiguration, trackSelection, bufferedDurationUs, + chunkDurationUs, CmcdLog.STREAMING_FORMAT_DASH, manifest.dynamic); - RepresentationHolder representationHolder = - updateSelectedBaseUrl(trackSelection.getSelectedIndex()); + RepresentationHolder representationHolder = updateSelectedBaseUrl(selectedTrackIndex); if (representationHolder.chunkExtractor != null) { Representation selectedRepresentation = representationHolder.representation; @Nullable RangedUri pendingInitializationUri = null; diff --git a/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DefaultDashChunkSourceTest.java b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DefaultDashChunkSourceTest.java index c1d17cad6a..d9bc88301c 100644 --- a/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DefaultDashChunkSourceTest.java +++ b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DefaultDashChunkSourceTest.java @@ -314,7 +314,7 @@ public class DefaultDashChunkSourceTest { assertThat(output.chunk.dataSpec.httpRequestHeaders) .containsExactly( "CMCD-Object", - "br=700", + "br=700,tb=1300,d=4000", "CMCD-Request", "bl=0", "CMCD-Session", @@ -359,7 +359,7 @@ public class DefaultDashChunkSourceTest { assertThat(output.chunk.dataSpec.httpRequestHeaders) .containsExactly( "CMCD-Object", - "br=700", + "br=700,tb=1300,d=4000", "CMCD-Request", "bl=0", "CMCD-Session", @@ -405,7 +405,7 @@ public class DefaultDashChunkSourceTest { assertThat(output.chunk.dataSpec.httpRequestHeaders) .containsExactly( "CMCD-Object", - "br=700,key1=value1", + "br=700,tb=1300,d=4000,key1=value1", "CMCD-Request", "bl=0,key2=\"stringValue\"", "CMCD-Session", 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 a0c32d1894..e328ca21d8 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 @@ -481,6 +481,13 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; seenExpectedPlaylistError = false; expectedPlaylistUrl = null; + long chunkDurationUs = 0; + if (selectedTrackIndex < mediaChunkIterators.length + && mediaChunkIterators[selectedTrackIndex].next()) { + chunkDurationUs = + mediaChunkIterators[selectedTrackIndex].getChunkEndTimeUs() + - mediaChunkIterators[selectedTrackIndex].getChunkStartTimeUs(); + } @Nullable CmcdLog cmcdLog = cmcdConfiguration == null @@ -489,6 +496,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; cmcdConfiguration, trackSelection, bufferedDurationUs, + chunkDurationUs, CmcdLog.STREAMING_FORMAT_HLS, !playlist.hasEndTag); diff --git a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsChunkSourceTest.java b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsChunkSourceTest.java index 41c3fa9380..1f6e243e11 100644 --- a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsChunkSourceTest.java +++ b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsChunkSourceTest.java @@ -210,7 +210,7 @@ public class HlsChunkSourceTest { assertThat(output.chunk.dataSpec.httpRequestHeaders) .containsExactly( "CMCD-Object", - "br=800", + "br=800,tb=800,d=0", "CMCD-Request", "bl=0", "CMCD-Session", @@ -256,7 +256,7 @@ public class HlsChunkSourceTest { assertThat(output.chunk.dataSpec.httpRequestHeaders) .containsExactly( "CMCD-Object", - "br=800", + "br=800,tb=800,d=0", "CMCD-Request", "bl=0", "CMCD-Session", @@ -303,7 +303,7 @@ public class HlsChunkSourceTest { assertThat(output.chunk.dataSpec.httpRequestHeaders) .containsExactly( "CMCD-Object", - "br=800,key1=value1", + "br=800,tb=800,d=0,key1=value1", "CMCD-Request", "bl=0,key2=\"stringValue\"", "CMCD-Session", 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 1f5babe94a..3871bb7644 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 @@ -280,6 +280,12 @@ public class DefaultSsChunkSource implements SsChunkSource { int manifestTrackIndex = trackSelection.getIndexInTrackGroup(trackSelectionIndex); Uri uri = streamElement.buildRequestUri(manifestTrackIndex, chunkIndex); + long chunkDurationUs = 0; + if (trackSelectionIndex < chunkIterators.length && chunkIterators[trackSelectionIndex].next()) { + chunkDurationUs = + chunkIterators[trackSelectionIndex].getChunkEndTimeUs() + - chunkIterators[trackSelectionIndex].getChunkStartTimeUs(); + } @Nullable CmcdLog cmcdLog = cmcdConfiguration == null @@ -288,6 +294,7 @@ public class DefaultSsChunkSource implements SsChunkSource { cmcdConfiguration, trackSelection, bufferedDurationUs, + chunkDurationUs, CmcdLog.STREAMING_FORMAT_SS, manifest.isLive); diff --git a/libraries/exoplayer_smoothstreaming/src/test/java/androidx/media3/exoplayer/smoothstreaming/DefaultSsChunkSourceTest.java b/libraries/exoplayer_smoothstreaming/src/test/java/androidx/media3/exoplayer/smoothstreaming/DefaultSsChunkSourceTest.java index 5846d07248..fa8a7d43a8 100644 --- a/libraries/exoplayer_smoothstreaming/src/test/java/androidx/media3/exoplayer/smoothstreaming/DefaultSsChunkSourceTest.java +++ b/libraries/exoplayer_smoothstreaming/src/test/java/androidx/media3/exoplayer/smoothstreaming/DefaultSsChunkSourceTest.java @@ -64,7 +64,7 @@ public class DefaultSsChunkSourceTest { assertThat(output.chunk.dataSpec.httpRequestHeaders) .containsExactly( "CMCD-Object", - "br=307", + "br=307,tb=1536,d=1968", "CMCD-Request", "bl=0", "CMCD-Session", @@ -109,7 +109,7 @@ public class DefaultSsChunkSourceTest { assertThat(output.chunk.dataSpec.httpRequestHeaders) .containsExactly( "CMCD-Object", - "br=307", + "br=307,tb=1536,d=1968", "CMCD-Request", "bl=0", "CMCD-Session", @@ -155,7 +155,7 @@ public class DefaultSsChunkSourceTest { assertThat(output.chunk.dataSpec.httpRequestHeaders) .containsExactly( "CMCD-Object", - "br=307,key1=value1", + "br=307,tb=1536,d=1968,key1=value1", "CMCD-Request", "bl=0,key2=\"stringValue\"", "CMCD-Session",