Add field object type (ot)

Added this CMCD-Object field to Common Media Client Data (CMCD) logging.

#minor-release

PiperOrigin-RevId: 554843305
This commit is contained in:
rohks 2023-08-08 16:09:07 +00:00 committed by Tianyi Feng
parent cba027c3b4
commit 3ec462d1cf
10 changed files with 190 additions and 41 deletions

View File

@ -37,7 +37,8 @@
(([#33](https://github.com/androidx/media/issues/33)),([#9978](https://github.com/google/ExoPlayer/issues/9978))).
* Add additional fields to Common Media Client Data (CMCD) logging:
streaming format (sf), stream type (st), version (v), top birate (tb),
object duration (d) and measured throughput (mtp).
object duration (d), measured throughput (mtp) and object type(ot)
([#8699](https://github.com/google/ExoPlayer/issues/8699)).
* Rename `MimeTypes.TEXT_EXOPLAYER_CUES` to
`MimeTypes.APPLICATION_MEDIA3_CUES`.
* Add `PngExtractor` that sends and reads a whole png file into the the

View File

@ -66,7 +66,8 @@ public final class CmcdConfiguration {
KEY_VERSION,
KEY_TOP_BITRATE,
KEY_OBJECT_DURATION,
KEY_MEASURED_THROUGHPUT
KEY_MEASURED_THROUGHPUT,
KEY_OBJECT_TYPE
})
@Documented
@Target(TYPE_USE)
@ -90,6 +91,7 @@ public final class CmcdConfiguration {
public static final String KEY_TOP_BITRATE = "tb";
public static final String KEY_OBJECT_DURATION = "d";
public static final String KEY_MEASURED_THROUGHPUT = "mtp";
public static final String KEY_OBJECT_TYPE = "ot";
/**
* Factory for {@link CmcdConfiguration} instances.
@ -291,4 +293,12 @@ public final class CmcdConfiguration {
public boolean isMeasuredThroughputLoggingAllowed() {
return requestConfig.isKeyAllowed(KEY_MEASURED_THROUGHPUT);
}
/**
* Returns whether logging object type is allowed based on the {@linkplain RequestConfig request
* configuration}.
*/
public boolean isObjectTypeLoggingAllowed() {
return requestConfig.isKeyAllowed(KEY_OBJECT_TYPE);
}
}

View File

@ -23,6 +23,8 @@ import android.text.TextUtils;
import androidx.annotation.Nullable;
import androidx.annotation.StringDef;
import androidx.media3.common.C;
import androidx.media3.common.C.TrackType;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.TrackGroup;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
@ -45,6 +47,33 @@ import java.lang.annotation.Target;
@UnstableApi
public final class CmcdHeadersFactory {
/**
* Retrieves the object type value from the given {@link ExoTrackSelection}.
*
* @param trackSelection The {@link ExoTrackSelection} from which to retrieve the object type.
* @return The object type value as a String if {@link TrackType} can be mapped to one of the
* object types specified by {@link ObjectType} annotation, or {@code null}.
* @throws IllegalArgumentException if the provided {@link ExoTrackSelection} is {@code null}.
*/
@Nullable
public static @ObjectType String getObjectType(ExoTrackSelection trackSelection) {
checkArgument(trackSelection != null);
@C.TrackType
int trackType = MimeTypes.getTrackType(trackSelection.getSelectedFormat().sampleMimeType);
if (trackType == C.TRACK_TYPE_UNKNOWN) {
trackType = MimeTypes.getTrackType(trackSelection.getSelectedFormat().containerMimeType);
}
if (trackType == C.TRACK_TYPE_AUDIO) {
return OBJECT_TYPE_AUDIO_ONLY;
} else if (trackType == C.TRACK_TYPE_VIDEO) {
return OBJECT_TYPE_VIDEO_ONLY;
} else {
// Track type cannot be mapped to a known object type.
return null;
}
}
/** Indicates the streaming format used for media content. */
@Retention(RetentionPolicy.SOURCE)
@StringDef({STREAMING_FORMAT_DASH, STREAMING_FORMAT_HLS, STREAMING_FORMAT_SS})
@ -59,6 +88,18 @@ public final class CmcdHeadersFactory {
@Target(TYPE_USE)
public @interface StreamType {}
/** Indicates the media type of current object being requested. */
@Retention(RetentionPolicy.SOURCE)
@StringDef({
OBJECT_TYPE_INIT_SEGMENT,
OBJECT_TYPE_AUDIO_ONLY,
OBJECT_TYPE_VIDEO_ONLY,
OBJECT_TYPE_MUXED_AUDIO_AND_VIDEO
})
@Documented
@Target(TYPE_USE)
public @interface ObjectType {}
/** Represents the Dynamic Adaptive Streaming over HTTP (DASH) format. */
public static final String STREAMING_FORMAT_DASH = "d";
@ -74,12 +115,25 @@ public final class CmcdHeadersFactory {
/** Represents the Live Streaming stream type. */
public static final String STREAM_TYPE_LIVE = "l";
/** Represents the object type for an initialization segment in a media container. */
public static final String OBJECT_TYPE_INIT_SEGMENT = "i";
/** Represents the object type for audio-only content in a media container. */
public static final String OBJECT_TYPE_AUDIO_ONLY = "a";
/** Represents the object type for video-only content in a media container. */
public static final String OBJECT_TYPE_VIDEO_ONLY = "v";
/** 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";
private final CmcdConfiguration cmcdConfiguration;
private final ExoTrackSelection trackSelection;
private final long bufferedDurationUs;
private final @StreamingFormat String streamingFormat;
private final boolean isLive;
private long chunkDurationUs;
private @Nullable @ObjectType String objectType;
/**
* Creates an instance.
@ -122,6 +176,18 @@ public final class CmcdHeadersFactory {
return this;
}
/**
* Sets the object type of the current object being requested. Must be one of the allowed object
* types specified by the {@link ObjectType} annotation.
*
* <p>Default is {@code null}.
*/
@CanIgnoreReturnValue
public CmcdHeadersFactory setObjectType(@Nullable @ObjectType String objectType) {
this.objectType = objectType;
return this;
}
/** Creates and returns a new {@link ImmutableMap} containing the CMCD HTTP request headers. */
public ImmutableMap<@CmcdConfiguration.HeaderKey String, String> createHttpRequestHeaders() {
ImmutableMap<@CmcdConfiguration.HeaderKey String, String> customData =
@ -130,24 +196,30 @@ public final class CmcdHeadersFactory {
CmcdObject.Builder cmcdObject =
new CmcdObject.Builder().setCustomData(customData.get(CmcdConfiguration.KEY_CMCD_OBJECT));
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);
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() && chunkDurationUs != C.TIME_UNSET) {
cmcdObject.setObjectDurationMs(chunkDurationUs / 1000);
}
cmcdObject.setTopBitrateKbps(Util.ceilDivide(topBitrate, 1000));
}
if (cmcdConfiguration.isObjectDurationLoggingAllowed() && chunkDurationUs != C.TIME_UNSET) {
cmcdObject.setObjectDurationMs(chunkDurationUs / 1000);
if (cmcdConfiguration.isObjectTypeLoggingAllowed()) {
cmcdObject.setObjectType(objectType);
}
CmcdRequest.Builder cmcdRequest =
new CmcdRequest.Builder().setCustomData(customData.get(CmcdConfiguration.KEY_CMCD_REQUEST));
if (cmcdConfiguration.isBufferLengthLoggingAllowed()) {
if (!getIsInitSegment() && cmcdConfiguration.isBufferLengthLoggingAllowed()) {
cmcdRequest.setBufferLengthMs(bufferedDurationUs / 1000);
}
if (cmcdConfiguration.isMeasuredThroughputLoggingAllowed()
@ -186,6 +258,10 @@ public final class CmcdHeadersFactory {
return httpRequestHeaders.buildOrThrow();
}
private boolean getIsInitSegment() {
return objectType != null && objectType.equals(OBJECT_TYPE_INIT_SEGMENT);
}
/** Keys whose values vary with the object being requested. Contains CMCD fields: {@code br}. */
private static final class CmcdObject {
@ -194,6 +270,7 @@ public final class CmcdHeadersFactory {
private int bitrateKbps;
private int topBitrateKbps;
private long objectDurationMs;
@Nullable private @ObjectType String objectType;
@Nullable private String customData;
/** Creates a new instance with default values. */
@ -231,6 +308,13 @@ public final class CmcdHeadersFactory {
return this;
}
/** Sets the {@link CmcdObject#objectType}. The default value is {@code null}. */
@CanIgnoreReturnValue
public Builder setObjectType(@Nullable @ObjectType String objectType) {
this.objectType = objectType;
return this;
}
/** Sets the {@link CmcdObject#customData}. The default value is {@code null}. */
@CanIgnoreReturnValue
public Builder setCustomData(@Nullable String customData) {
@ -268,6 +352,14 @@ public final class CmcdHeadersFactory {
*/
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.
*/
@Nullable public final @ObjectType String objectType;
/**
* Custom data where the values of the keys vary with the object being requested, or {@code
* null} if unset.
@ -281,6 +373,7 @@ public final class CmcdHeadersFactory {
this.bitrateKbps = builder.bitrateKbps;
this.topBitrateKbps = builder.topBitrateKbps;
this.objectDurationMs = builder.objectDurationMs;
this.objectType = builder.objectType;
this.customData = builder.customData;
}
@ -306,6 +399,10 @@ public final class CmcdHeadersFactory {
Util.formatInvariant(
"%s=%d,", CmcdConfiguration.KEY_OBJECT_DURATION, objectDurationMs));
}
if (!TextUtils.isEmpty(objectType)) {
headerValue.append(
Util.formatInvariant("%s=%s,", CmcdConfiguration.KEY_OBJECT_TYPE, objectType));
}
if (!TextUtils.isEmpty(customData)) {
headerValue.append(Util.formatInvariant("%s,", customData));
}
@ -377,6 +474,10 @@ public final class CmcdHeadersFactory {
* The buffer length in milliseconds associated with the media object being requested, or {@link
* C#TIME_UNSET} if unset.
*
* <p>This key SHOULD only be sent with an {@link CmcdObject#objectType} of {@link
* #OBJECT_TYPE_AUDIO_ONLY}, {@link #OBJECT_TYPE_VIDEO_ONLY} or {@link
* #OBJECT_TYPE_MUXED_AUDIO_AND_VIDEO}.
*
* <p>This value MUST be rounded to the nearest 100 ms.
*/
public final long bufferLengthMs;
@ -531,15 +632,16 @@ public final class CmcdHeadersFactory {
@Nullable public final String sessionId;
/**
* The streaming format that defines the current request. d = MPEG DASH, h = HTTP Live Streaming
* (HLS), s = Smooth Streaming and o = other. If the streaming format being requested is
* unknown, then this key MUST NOT be used.
* The streaming format that defines the current request , or {@code null} if unset. Must be one
* of the allowed streaming formats specified by the {@link StreamingFormat} annotation.
*
* <p>If the streaming format being requested is unknown, then this key MUST NOT be used.
*/
@Nullable public final @StreamingFormat String streamingFormat;
/**
* Type of stream. v = all segments are available e.g., VOD and l = segments become available
* over time e.g., LIVE.
* Type of stream, or {@code null} if unset. Must be one of the allowed stream types specified
* by the {@link StreamType} annotation.
*/
@Nullable public final @StreamType String streamType;

View File

@ -662,7 +662,9 @@ public class DefaultDashChunkSource implements DashChunkSource {
ImmutableMap<@CmcdConfiguration.HeaderKey String, String> httpRequestHeaders =
cmcdHeadersFactory == null
? ImmutableMap.of()
: cmcdHeadersFactory.createHttpRequestHeaders();
: cmcdHeadersFactory
.setObjectType(CmcdHeadersFactory.OBJECT_TYPE_INIT_SEGMENT)
.createHttpRequestHeaders();
DataSpec dataSpec =
DashUtil.buildDataSpec(
representation,
@ -706,6 +708,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
? ImmutableMap.of()
: cmcdHeadersFactory
.setChunkDurationUs(endTimeUs - startTimeUs)
.setObjectType(CmcdHeadersFactory.getObjectType(trackSelection))
.createHttpRequestHeaders();
DataSpec dataSpec =
DashUtil.buildDataSpec(
@ -755,6 +758,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
? ImmutableMap.of()
: cmcdHeadersFactory
.setChunkDurationUs(endTimeUs - startTimeUs)
.setObjectType(CmcdHeadersFactory.getObjectType(trackSelection))
.createHttpRequestHeaders();
DataSpec dataSpec =
DashUtil.buildDataSpec(

View File

@ -314,7 +314,7 @@ public class DefaultDashChunkSourceTest {
assertThat(output.chunk.dataSpec.httpRequestHeaders)
.containsExactly(
"CMCD-Object",
"br=700,tb=1300,d=4000",
"br=700,tb=1300,d=4000,ot=v",
"CMCD-Request",
"bl=0,mtp=1000",
"CMCD-Session",
@ -359,7 +359,7 @@ public class DefaultDashChunkSourceTest {
assertThat(output.chunk.dataSpec.httpRequestHeaders)
.containsExactly(
"CMCD-Object",
"br=700,tb=1300,d=4000",
"br=700,tb=1300,d=4000,ot=v",
"CMCD-Request",
"bl=0,mtp=1000",
"CMCD-Session",
@ -405,7 +405,7 @@ public class DefaultDashChunkSourceTest {
assertThat(output.chunk.dataSpec.httpRequestHeaders)
.containsExactly(
"CMCD-Object",
"br=700,tb=1300,d=4000,key1=value1",
"br=700,tb=1300,d=4000,ot=v,key1=value1",
"CMCD-Request",
"bl=0,mtp=1000,key2=\"stringValue\"",
"CMCD-Session",

View File

@ -27,6 +27,7 @@ import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.TrackGroup;
import androidx.media3.common.util.TimestampAdjuster;
import androidx.media3.common.util.UriUtil;
@ -486,25 +487,31 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
cmcdConfiguration == null
? null
: new CmcdHeadersFactory(
cmcdConfiguration,
trackSelection,
bufferedDurationUs,
/* streamingFormat= */ CmcdHeadersFactory.STREAMING_FORMAT_HLS,
/* isLive= */ !playlist.hasEndTag);
cmcdConfiguration,
trackSelection,
bufferedDurationUs,
/* streamingFormat= */ CmcdHeadersFactory.STREAMING_FORMAT_HLS,
/* isLive= */ !playlist.hasEndTag)
.setObjectType(
getIsMuxedAudioAndVideo()
? CmcdHeadersFactory.OBJECT_TYPE_MUXED_AUDIO_AND_VIDEO
: CmcdHeadersFactory.getObjectType(trackSelection));
// Check if the media segment or its initialization segment are fully encrypted.
@Nullable
Uri initSegmentKeyUri =
getFullEncryptionKeyUri(playlist, segmentBaseHolder.segmentBase.initializationSegment);
out.chunk =
maybeCreateEncryptionChunkFor(initSegmentKeyUri, selectedTrackIndex, cmcdHeadersFactory);
maybeCreateEncryptionChunkFor(
initSegmentKeyUri, selectedTrackIndex, /* isInitSegment= */ true, cmcdHeadersFactory);
if (out.chunk != null) {
return;
}
@Nullable
Uri mediaSegmentKeyUri = getFullEncryptionKeyUri(playlist, segmentBaseHolder.segmentBase);
out.chunk =
maybeCreateEncryptionChunkFor(mediaSegmentKeyUri, selectedTrackIndex, cmcdHeadersFactory);
maybeCreateEncryptionChunkFor(
mediaSegmentKeyUri, selectedTrackIndex, /* isInitSegment= */ false, cmcdHeadersFactory);
if (out.chunk != null) {
return;
}
@ -543,6 +550,13 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
cmcdHeadersFactory);
}
private boolean getIsMuxedAudioAndVideo() {
Format format = trackGroup.getFormat(trackSelection.getSelectedIndex());
String audioMimeType = MimeTypes.getAudioMediaMimeType(format.codecs);
String videoMimeType = MimeTypes.getVideoMediaMimeType(format.codecs);
return audioMimeType != null && videoMimeType != null;
}
@Nullable
private static SegmentBaseHolder getNextSegmentHolder(
HlsMediaPlaylist mediaPlaylist, long nextMediaSequence, int nextPartIndex) {
@ -850,6 +864,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private Chunk maybeCreateEncryptionChunkFor(
@Nullable Uri keyUri,
int selectedTrackIndex,
boolean isInitSegment,
@Nullable CmcdHeadersFactory cmcdHeadersFactory) {
if (keyUri == null) {
return null;
@ -863,10 +878,15 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
keyCache.put(keyUri, encryptionKey);
return null;
}
ImmutableMap<@CmcdConfiguration.HeaderKey String, String> httpRequestHeaders =
cmcdHeadersFactory == null
? ImmutableMap.of()
: cmcdHeadersFactory.createHttpRequestHeaders();
ImmutableMap.of();
if (cmcdHeadersFactory != null) {
if (isInitSegment) {
cmcdHeadersFactory.setObjectType(CmcdHeadersFactory.OBJECT_TYPE_INIT_SEGMENT);
}
httpRequestHeaders = cmcdHeadersFactory.createHttpRequestHeaders();
}
DataSpec dataSpec =
new DataSpec.Builder()
.setUri(keyUri)

View File

@ -141,8 +141,19 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
? getEncryptionIvArray(Assertions.checkNotNull(initSegment.encryptionIV))
: null;
Uri initSegmentUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, initSegment.url);
ImmutableMap<@CmcdConfiguration.HeaderKey String, String> initHttpRequestHeaders =
cmcdHeadersFactory == null
? ImmutableMap.of()
: cmcdHeadersFactory
.setObjectType(CmcdHeadersFactory.OBJECT_TYPE_INIT_SEGMENT)
.createHttpRequestHeaders();
initDataSpec =
new DataSpec(initSegmentUri, initSegment.byteRangeOffset, initSegment.byteRangeLength);
new DataSpec.Builder()
.setUri(initSegmentUri)
.setPosition(initSegment.byteRangeOffset)
.setLength(initSegment.byteRangeLength)
.setHttpRequestHeaders(initHttpRequestHeaders)
.build();
initDataSource = buildDataSource(dataSource, initSegmentKey, initSegmentIv);
}

View File

@ -210,7 +210,7 @@ public class HlsChunkSourceTest {
assertThat(output.chunk.dataSpec.httpRequestHeaders)
.containsExactly(
"CMCD-Object",
"br=800,tb=800,d=4000",
"br=800,tb=800,d=4000,ot=v",
"CMCD-Request",
"bl=0",
"CMCD-Session",
@ -256,7 +256,7 @@ public class HlsChunkSourceTest {
assertThat(output.chunk.dataSpec.httpRequestHeaders)
.containsExactly(
"CMCD-Object",
"br=800,tb=800,d=4000",
"br=800,tb=800,d=4000,ot=v",
"CMCD-Request",
"bl=0",
"CMCD-Session",
@ -303,7 +303,7 @@ public class HlsChunkSourceTest {
assertThat(output.chunk.dataSpec.httpRequestHeaders)
.containsExactly(
"CMCD-Object",
"br=800,tb=800,d=4000,key1=value1",
"br=800,tb=800,d=4000,ot=v,key1=value1",
"CMCD-Request",
"bl=0,key2=\"stringValue\"",
"CMCD-Session",

View File

@ -290,7 +290,8 @@ public class DefaultSsChunkSource implements SsChunkSource {
bufferedDurationUs,
/* streamingFormat= */ CmcdHeadersFactory.STREAMING_FORMAT_SS,
/* isLive= */ manifest.isLive)
.setChunkDurationUs(chunkEndTimeUs - chunkStartTimeUs);
.setChunkDurationUs(chunkEndTimeUs - chunkStartTimeUs)
.setObjectType(CmcdHeadersFactory.getObjectType(trackSelection));
out.chunk =
newMediaChunk(

View File

@ -64,7 +64,7 @@ public class DefaultSsChunkSourceTest {
assertThat(output.chunk.dataSpec.httpRequestHeaders)
.containsExactly(
"CMCD-Object",
"br=308,tb=1536,d=1968",
"br=308,tb=1536,d=1968,ot=v",
"CMCD-Request",
"bl=0,mtp=1000",
"CMCD-Session",
@ -109,7 +109,7 @@ public class DefaultSsChunkSourceTest {
assertThat(output.chunk.dataSpec.httpRequestHeaders)
.containsExactly(
"CMCD-Object",
"br=308,tb=1536,d=1968",
"br=308,tb=1536,d=1968,ot=v",
"CMCD-Request",
"bl=0,mtp=1000",
"CMCD-Session",
@ -155,7 +155,7 @@ public class DefaultSsChunkSourceTest {
assertThat(output.chunk.dataSpec.httpRequestHeaders)
.containsExactly(
"CMCD-Object",
"br=308,tb=1536,d=1968,key1=value1",
"br=308,tb=1536,d=1968,ot=v,key1=value1",
"CMCD-Request",
"bl=0,mtp=1000,key2=\"stringValue\"",
"CMCD-Session",