CmcdConfiguration.java

/*
 * Copyright 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package androidx.media3.exoplayer.upstream;

import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static java.lang.annotation.ElementType.TYPE_USE;

import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.annotation.StringDef;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.common.util.UnstableApi;
import com.google.common.collect.ImmutableListMultimap;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.UUID;

/** Represents a configuration for the Common Media Client Data (CMCD) logging. */
@UnstableApi
public final class CmcdConfiguration {

  /**
   * Header keys SHOULD be allocated to one of the four defined header names based upon their
   * expected level of variability:
   *
   * <ul>
   *   <li>CMCD-Object: keys whose values vary with the object being requested.
   *   <li>CMCD-Request: keys whose values vary with each request.
   *   <li>CMCD-Session: keys whose values are expected to be invariant over the life of the
   *       session.
   *   <li>CMCD-Status: keys whose values do not vary with every request or object.
   * </ul>
   */
  @Retention(RetentionPolicy.SOURCE)
  @StringDef({KEY_CMCD_OBJECT, KEY_CMCD_REQUEST, KEY_CMCD_SESSION, KEY_CMCD_STATUS})
  @Documented
  @Target(TYPE_USE)
  public @interface HeaderKey {}

  /** Indicates that the annotated element represents a CMCD key. */
  @Retention(RetentionPolicy.SOURCE)
  @StringDef({
    KEY_BITRATE,
    KEY_BUFFER_LENGTH,
    KEY_CONTENT_ID,
    KEY_SESSION_ID,
    KEY_MAXIMUM_REQUESTED_BITRATE,
    KEY_STREAMING_FORMAT,
    KEY_STREAM_TYPE,
    KEY_VERSION,
    KEY_TOP_BITRATE,
    KEY_OBJECT_DURATION,
    KEY_MEASURED_THROUGHPUT,
    KEY_OBJECT_TYPE,
    KEY_BUFFER_STARVATION,
    KEY_DEADLINE,
    KEY_PLAYBACK_RATE,
    KEY_STARTUP,
    KEY_NEXT_OBJECT_REQUEST,
    KEY_NEXT_RANGE_REQUEST
  })
  @Documented
  @Target(TYPE_USE)
  public @interface CmcdKey {}

  /** Indicates the mode used for data transmission. */
  @Retention(RetentionPolicy.SOURCE)
  @IntDef({MODE_REQUEST_HEADER, MODE_QUERY_PARAMETER})
  @Documented
  @Target(TYPE_USE)
  public @interface DataTransmissionMode {}

  /** Maximum length for ID fields. */
  public static final int MAX_ID_LENGTH = 64;

  public static final String KEY_CMCD_OBJECT = "CMCD-Object";
  public static final String KEY_CMCD_REQUEST = "CMCD-Request";
  public static final String KEY_CMCD_SESSION = "CMCD-Session";
  public static final String KEY_CMCD_STATUS = "CMCD-Status";
  public static final String CMCD_QUERY_PARAMETER_KEY = "CMCD";
  public static final String KEY_BITRATE = "br";
  public static final String KEY_BUFFER_LENGTH = "bl";
  public static final String KEY_CONTENT_ID = "cid";
  public static final String KEY_SESSION_ID = "sid";
  public static final String KEY_MAXIMUM_REQUESTED_BITRATE = "rtp";
  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";
  public static final String KEY_MEASURED_THROUGHPUT = "mtp";
  public static final String KEY_OBJECT_TYPE = "ot";
  public static final String KEY_BUFFER_STARVATION = "bs";
  public static final String KEY_DEADLINE = "dl";
  public static final String KEY_PLAYBACK_RATE = "pr";
  public static final String KEY_STARTUP = "su";
  public static final String KEY_NEXT_OBJECT_REQUEST = "nor";
  public static final String KEY_NEXT_RANGE_REQUEST = "nrr";
  public static final int MODE_REQUEST_HEADER = 0;
  public static final int MODE_QUERY_PARAMETER = 1;

  /**
   * Factory for {@link CmcdConfiguration} instances.
   *
   * <p>Implementations must not make assumptions about which thread called their methods; and must
   * be thread-safe.
   */
  public interface Factory {
    /**
     * Creates a {@link CmcdConfiguration} based on the provided {@link MediaItem}.
     *
     * @param mediaItem The {@link MediaItem} from which to create the CMCD configuration.
     * @return A {@link CmcdConfiguration} instance.
     */
    CmcdConfiguration createCmcdConfiguration(MediaItem mediaItem);

    /**
     * The default factory implementation.
     *
     * <p>It creates a {@link CmcdConfiguration} by generating a random session ID and using the
     * content ID from {@link MediaItem#mediaId} (or {@link MediaItem#DEFAULT_MEDIA_ID} if the media
     * item does not have a {@link MediaItem#mediaId} defined).
     *
     * <p>It also utilises a default {@link RequestConfig} implementation that enables all available
     * keys, provides empty custom data, and sets the maximum requested bitrate to {@link
     * C#RATE_UNSET_INT}.
     */
    CmcdConfiguration.Factory DEFAULT =
        mediaItem ->
            new CmcdConfiguration(
                /* sessionId= */ UUID.randomUUID().toString(),
                /* contentId= */ mediaItem.mediaId != null
                    ? mediaItem.mediaId
                    : MediaItem.DEFAULT_MEDIA_ID,
                new RequestConfig() {});
  }

  /**
   * Represents configuration which can vary on each request.
   *
   * <p>Implementations must not make assumptions about which thread called their methods; and must
   * be thread-safe.
   */
  public interface RequestConfig {
    /**
     * Checks whether the specified key is allowed in CMCD logging. By default, all keys are
     * allowed.
     *
     * @param key The key to check.
     * @return Whether the key is allowed.
     */
    default boolean isKeyAllowed(@CmcdKey String key) {
      return true;
    }

    /**
     * Retrieves the custom data associated with CMCD logging.
     *
     * <p>By default, no custom data is provided.
     *
     * <p>The data payload consists of a series of key/value pairs constructed according to the
     * following rules:
     *
     * <ul>
     *   <li>Custom keys SHOULD be allocated to one of the four defined header names defined in the
     *       {@link HeaderKey} annotation.
     *   <li>All information in the payload MUST be represented as key=value pairs.
     *   <li>The key and value MUST be separated by an equals sign. If the value type is boolean and
     *       the value is {@code true}, then the equals sign and the value MUST be omitted.
     *   <li>The key names are case-sensitive and reserved. Custom key names MUST carry a hyphenated
     *       prefix to ensure no namespace collision with future revisions to Common Media Client
     *       Data (CMCD) specification. Clients SHOULD use a reverse-DNS syntax when defining their
     *       own prefix.
     *   <li>Any value of type String MUST be enclosed by opening and closing double quotes. Double
     *       quotes and backslashes MUST be escaped using a backslash "\" character. Any value that
     *       is not of type string does not require quoting.
     * </ul>
     *
     * <p><b>Note:</b> The key words MUST and SHOULD are to be interpreted as described in RFC 2119.
     *
     * <p>Example:
     *
     * <ul>
     *   <li>CMCD-Request:custom-field1=25400
     *   <li>CMCD-Object:custom-field2=3200,custom-field3=4004,custom-field4=v,custom-field5=6000
     *   <li>CMCD-Status:custom-field6,custom-field7=15000
     *   <li>CMCD-Session:custom-field8="stringValue"
     * </ul>
     *
     * @return An {@link ImmutableListMultimap} containing the custom data.
     */
    default ImmutableListMultimap<@HeaderKey String, String> getCustomData() {
      return ImmutableListMultimap.of();
    }

    /**
     * Returns the maximum throughput requested in kbps, or {@link C#RATE_UNSET_INT} if the maximum
     * throughput is unknown in which case the maximum throughput will not be logged upstream.
     *
     * @param throughputKbps The throughput in kbps of the audio or video object being requested.
     * @return The maximum throughput requested in kbps.
     */
    default int getRequestedMaximumThroughputKbps(int throughputKbps) {
      return C.RATE_UNSET_INT;
    }
  }

  /**
   * A GUID identifying the current playback session, or {@code null} if unset.
   *
   * <p>A playback session typically ties together segments belonging to a single media asset.
   * Maximum length is 64 characters.
   */
  @Nullable public final String sessionId;

  /**
   * A GUID identifying the current content, or {@code null} if unset.
   *
   * <p>This value is consistent across multiple different sessions and devices and is defined and
   * updated at the discretion of the service provider. Maximum length is 64 characters.
   */
  @Nullable public final String contentId;

  /** Dynamic request specific configuration. */
  public final RequestConfig requestConfig;

  /** Mode used for data transmission. */
  public final @DataTransmissionMode int dataTransmissionMode;

  /** Creates an instance with {@link #dataTransmissionMode} set to {@link #MODE_REQUEST_HEADER}. */
  public CmcdConfiguration(
      @Nullable String sessionId, @Nullable String contentId, RequestConfig requestConfig) {
    this(sessionId, contentId, requestConfig, MODE_REQUEST_HEADER);
  }

  /** Creates an instance. */
  public CmcdConfiguration(
      @Nullable String sessionId,
      @Nullable String contentId,
      RequestConfig requestConfig,
      @DataTransmissionMode int dataTransmissionMode) {
    checkArgument(sessionId == null || sessionId.length() <= MAX_ID_LENGTH);
    checkArgument(contentId == null || contentId.length() <= MAX_ID_LENGTH);
    checkNotNull(requestConfig);
    this.sessionId = sessionId;
    this.contentId = contentId;
    this.requestConfig = requestConfig;
    this.dataTransmissionMode = dataTransmissionMode;
  }

  /**
   * Returns whether logging bitrate is allowed based on the {@linkplain RequestConfig request
   * configuration}.
   */
  public boolean isBitrateLoggingAllowed() {
    return requestConfig.isKeyAllowed(KEY_BITRATE);
  }

  /**
   * Returns whether logging buffer length is allowed based on the {@linkplain RequestConfig request
   * configuration}.
   */
  public boolean isBufferLengthLoggingAllowed() {
    return requestConfig.isKeyAllowed(KEY_BUFFER_LENGTH);
  }

  /**
   * Returns whether logging content ID is allowed based on the {@linkplain RequestConfig request
   * configuration}.
   */
  public boolean isContentIdLoggingAllowed() {
    return requestConfig.isKeyAllowed(KEY_CONTENT_ID);
  }

  /**
   * Returns whether logging session ID is allowed based on the {@linkplain RequestConfig request
   * configuration}.
   */
  public boolean isSessionIdLoggingAllowed() {
    return requestConfig.isKeyAllowed(KEY_SESSION_ID);
  }

  /**
   * Returns whether logging maximum requested throughput is allowed based on the {@linkplain
   * RequestConfig request configuration}.
   */
  public boolean isMaximumRequestThroughputLoggingAllowed() {
    return requestConfig.isKeyAllowed(KEY_MAXIMUM_REQUESTED_BITRATE);
  }

  /**
   * Returns whether logging streaming format is allowed based on the {@linkplain RequestConfig
   * request configuration}.
   */
  public boolean isStreamingFormatLoggingAllowed() {
    return requestConfig.isKeyAllowed(KEY_STREAMING_FORMAT);
  }

  /**
   * Returns whether logging stream type is allowed based on the {@linkplain RequestConfig request
   * configuration}.
   */
  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);
  }

  /**
   * Returns whether logging measured throughput is allowed based on the {@linkplain RequestConfig
   * request configuration}.
   */
  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);
  }

  /**
   * Returns whether logging buffer starvation is allowed based on the {@linkplain RequestConfig
   * request configuration}.
   */
  public boolean isBufferStarvationLoggingAllowed() {
    return requestConfig.isKeyAllowed(KEY_BUFFER_STARVATION);
  }

  /**
   * Returns whether logging deadline is allowed based on the {@linkplain RequestConfig request
   * configuration}.
   */
  public boolean isDeadlineLoggingAllowed() {
    return requestConfig.isKeyAllowed(KEY_DEADLINE);
  }

  /**
   * Returns whether logging playback rate is allowed based on the {@linkplain RequestConfig request
   * configuration}.
   */
  public boolean isPlaybackRateLoggingAllowed() {
    return requestConfig.isKeyAllowed(KEY_PLAYBACK_RATE);
  }

  /**
   * Returns whether logging startup is allowed based on the {@linkplain RequestConfig request
   * configuration}.
   */
  public boolean isStartupLoggingAllowed() {
    return requestConfig.isKeyAllowed(KEY_STARTUP);
  }

  /**
   * Returns whether logging next object request is allowed based on the {@linkplain RequestConfig
   * request configuration}.
   */
  public boolean isNextObjectRequestLoggingAllowed() {
    return requestConfig.isKeyAllowed(KEY_NEXT_OBJECT_REQUEST);
  }

  /**
   * Returns whether logging next range request is allowed based on the {@linkplain RequestConfig
   * request configuration}.
   */
  public boolean isNextRangeRequestLoggingAllowed() {
    return requestConfig.isKeyAllowed(KEY_NEXT_RANGE_REQUEST);
  }
}