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
This commit is contained in:
rohks 2023-07-18 11:24:34 +01:00 committed by Ian Baker
parent 0fa66534fe
commit 1b2a2fcde0
10 changed files with 143 additions and 16 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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