RtspHeaders.java

/*
 * Copyright 2021 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.rtsp;

import androidx.annotation.Nullable;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.common.base.Ascii;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.Iterables;
import java.util.List;
import java.util.Map;

/**
 * RTSP message headers.
 *
 * <p>{@link Builder} must be used to construct an instance. Use {@link #get} to query header values
 * with case-insensitive header names. The extra spaces around header names and values are trimmed.
 * Contrary to HTTP, RTSP does not allow ambiguous/arbitrary header names (RFC 2326 Section 12).
 */
@UnstableApi
/* package */ final class RtspHeaders {

  public static final String ACCEPT = "Accept";
  public static final String ALLOW = "Allow";
  public static final String AUTHORIZATION = "Authorization";
  public static final String BANDWIDTH = "Bandwidth";
  public static final String BLOCKSIZE = "Blocksize";
  public static final String CACHE_CONTROL = "Cache-Control";
  public static final String CONNECTION = "Connection";
  public static final String CONTENT_BASE = "Content-Base";
  public static final String CONTENT_ENCODING = "Content-Encoding";
  public static final String CONTENT_LANGUAGE = "Content-Language";
  public static final String CONTENT_LENGTH = "Content-Length";
  public static final String CONTENT_LOCATION = "Content-Location";
  public static final String CONTENT_TYPE = "Content-Type";
  public static final String CSEQ = "CSeq";
  public static final String DATE = "Date";
  public static final String EXPIRES = "Expires";
  public static final String LOCATION = "Location";
  public static final String PROXY_AUTHENTICATE = "Proxy-Authenticate";
  public static final String PROXY_REQUIRE = "Proxy-Require";
  public static final String PUBLIC = "Public";
  public static final String RANGE = "Range";
  public static final String RTP_INFO = "RTP-Info";
  public static final String RTCP_INTERVAL = "RTCP-Interval";
  public static final String SCALE = "Scale";
  public static final String SESSION = "Session";
  public static final String SPEED = "Speed";
  public static final String SUPPORTED = "Supported";
  public static final String TIMESTAMP = "Timestamp";
  public static final String TRANSPORT = "Transport";
  public static final String USER_AGENT = "User-Agent";
  public static final String VIA = "Via";
  public static final String WWW_AUTHENTICATE = "WWW-Authenticate";

  /** An empty header object. */
  public static final RtspHeaders EMPTY = new RtspHeaders.Builder().build();

  /** Builds {@link RtspHeaders} instances. */
  public static final class Builder {
    private final ImmutableListMultimap.Builder<String, String> namesAndValuesBuilder;

    /** Creates a new instance. */
    public Builder() {
      namesAndValuesBuilder = new ImmutableListMultimap.Builder<>();
    }

    /**
     * Creates a new instance with common header values.
     *
     * @param userAgent The user agent string.
     * @param sessionId The RTSP session ID; use {@code null} when the session is not yet set up.
     * @param cSeq The RTSP cSeq sequence number.
     */
    public Builder(String userAgent, @Nullable String sessionId, int cSeq) {
      this();

      add(USER_AGENT, userAgent);
      add(CSEQ, String.valueOf(cSeq));
      if (sessionId != null) {
        add(SESSION, sessionId);
      }
    }

    /**
     * Creates a new instance to build upon the provided {@link RtspHeaders}.
     *
     * @param namesAndValuesBuilder A {@link ImmutableListMultimap.Builder} that this builder builds
     *     upon.
     */
    private Builder(ImmutableListMultimap.Builder<String, String> namesAndValuesBuilder) {
      this.namesAndValuesBuilder = namesAndValuesBuilder;
    }

    /**
     * Adds a header name and header value pair.
     *
     * @param headerName The name of the header.
     * @param headerValue The value of the header.
     * @return This builder.
     */
    public Builder add(String headerName, String headerValue) {
      namesAndValuesBuilder.put(convertToStandardHeaderName(headerName.trim()), headerValue.trim());
      return this;
    }

    /**
     * Adds a list of headers.
     *
     * @param headers The list of headers, each item must following the format &lt;headerName&gt;:
     *     &lt;headerValue&gt;
     * @return This builder.
     */
    public Builder addAll(List<String> headers) {
      for (int i = 0; i < headers.size(); i++) {
        String[] header = Util.splitAtFirst(headers.get(i), ":\s?");
        if (header.length == 2) {
          add(header[0], header[1]);
        }
      }
      return this;
    }

    /**
     * Adds multiple headers in a map.
     *
     * @param headers The map of headers, where the keys are the header names and the values are the
     *     header values.
     * @return This builder.
     */
    public Builder addAll(Map<String, String> headers) {
      for (Map.Entry<String, String> header : headers.entrySet()) {
        add(header.getKey(), header.getValue());
      }
      return this;
    }

    /**
     * Builds a new {@link RtspHeaders} instance.
     *
     * @return The newly built {@link RtspHeaders} instance.
     */
    public RtspHeaders build() {
      return new RtspHeaders(this);
    }
  }

  private final ImmutableListMultimap<String, String> namesAndValues;

  @Override
  public boolean equals(@Nullable Object obj) {
    if (this == obj) {
      return true;
    }
    if (!(obj instanceof RtspHeaders)) {
      return false;
    }
    RtspHeaders headers = (RtspHeaders) obj;
    return namesAndValues.equals(headers.namesAndValues);
  }

  @Override
  public int hashCode() {
    return namesAndValues.hashCode();
  }

  /** Returns a {@link Builder} initialized with the values of this instance. */
  public Builder buildUpon() {
    ImmutableListMultimap.Builder<String, String> namesAndValuesBuilder =
        new ImmutableListMultimap.Builder<>();
    namesAndValuesBuilder.putAll(namesAndValues);
    return new Builder(namesAndValuesBuilder);
  }

  /**
   * Returns a map that associates header names to the list of values associated with the
   * corresponding header name.
   */
  public ImmutableListMultimap<String, String> asMultiMap() {
    return namesAndValues;
  }

  /**
   * Returns the most recent header value mapped to the argument, {@code null} if the header name is
   * not recorded.
   */
  @Nullable
  public String get(String headerName) {
    ImmutableList<String> headerValues = values(headerName);
    if (headerValues.isEmpty()) {
      return null;
    }
    return Iterables.getLast(headerValues);
  }

  /**
   * Returns a list of header values mapped to the argument, in the addition order. The returned
   * list is empty if the header name is not recorded.
   */
  public ImmutableList<String> values(String headerName) {
    return namesAndValues.get(convertToStandardHeaderName(headerName));
  }

  private RtspHeaders(Builder builder) {
    this.namesAndValues = builder.namesAndValuesBuilder.build();
  }

  private static String convertToStandardHeaderName(String messageHeaderName) {
    if (Ascii.equalsIgnoreCase(messageHeaderName, ACCEPT)) {
      return ACCEPT;
    } else if (Ascii.equalsIgnoreCase(messageHeaderName, ALLOW)) {
      return ALLOW;
    } else if (Ascii.equalsIgnoreCase(messageHeaderName, AUTHORIZATION)) {
      return AUTHORIZATION;
    } else if (Ascii.equalsIgnoreCase(messageHeaderName, BANDWIDTH)) {
      return BANDWIDTH;
    } else if (Ascii.equalsIgnoreCase(messageHeaderName, BLOCKSIZE)) {
      return BLOCKSIZE;
    } else if (Ascii.equalsIgnoreCase(messageHeaderName, CACHE_CONTROL)) {
      return CACHE_CONTROL;
    } else if (Ascii.equalsIgnoreCase(messageHeaderName, CONNECTION)) {
      return CONNECTION;
    } else if (Ascii.equalsIgnoreCase(messageHeaderName, CONTENT_BASE)) {
      return CONTENT_BASE;
    } else if (Ascii.equalsIgnoreCase(messageHeaderName, CONTENT_ENCODING)) {
      return CONTENT_ENCODING;
    } else if (Ascii.equalsIgnoreCase(messageHeaderName, CONTENT_LANGUAGE)) {
      return CONTENT_LANGUAGE;
    } else if (Ascii.equalsIgnoreCase(messageHeaderName, CONTENT_LENGTH)) {
      return CONTENT_LENGTH;
    } else if (Ascii.equalsIgnoreCase(messageHeaderName, CONTENT_LOCATION)) {
      return CONTENT_LOCATION;
    } else if (Ascii.equalsIgnoreCase(messageHeaderName, CONTENT_TYPE)) {
      return CONTENT_TYPE;
    } else if (Ascii.equalsIgnoreCase(messageHeaderName, CSEQ)) {
      return CSEQ;
    } else if (Ascii.equalsIgnoreCase(messageHeaderName, DATE)) {
      return DATE;
    } else if (Ascii.equalsIgnoreCase(messageHeaderName, EXPIRES)) {
      return EXPIRES;
    } else if (Ascii.equalsIgnoreCase(messageHeaderName, LOCATION)) {
      return LOCATION;
    } else if (Ascii.equalsIgnoreCase(messageHeaderName, PROXY_AUTHENTICATE)) {
      return PROXY_AUTHENTICATE;
    } else if (Ascii.equalsIgnoreCase(messageHeaderName, PROXY_REQUIRE)) {
      return PROXY_REQUIRE;
    } else if (Ascii.equalsIgnoreCase(messageHeaderName, PUBLIC)) {
      return PUBLIC;
    } else if (Ascii.equalsIgnoreCase(messageHeaderName, RANGE)) {
      return RANGE;
    } else if (Ascii.equalsIgnoreCase(messageHeaderName, RTP_INFO)) {
      return RTP_INFO;
    } else if (Ascii.equalsIgnoreCase(messageHeaderName, RTCP_INTERVAL)) {
      return RTCP_INTERVAL;
    } else if (Ascii.equalsIgnoreCase(messageHeaderName, SCALE)) {
      return SCALE;
    } else if (Ascii.equalsIgnoreCase(messageHeaderName, SESSION)) {
      return SESSION;
    } else if (Ascii.equalsIgnoreCase(messageHeaderName, SPEED)) {
      return SPEED;
    } else if (Ascii.equalsIgnoreCase(messageHeaderName, SUPPORTED)) {
      return SUPPORTED;
    } else if (Ascii.equalsIgnoreCase(messageHeaderName, TIMESTAMP)) {
      return TIMESTAMP;
    } else if (Ascii.equalsIgnoreCase(messageHeaderName, TRANSPORT)) {
      return TRANSPORT;
    } else if (Ascii.equalsIgnoreCase(messageHeaderName, USER_AGENT)) {
      return USER_AGENT;
    } else if (Ascii.equalsIgnoreCase(messageHeaderName, VIA)) {
      return VIA;
    } else if (Ascii.equalsIgnoreCase(messageHeaderName, WWW_AUTHENTICATE)) {
      return WWW_AUTHENTICATE;
    }
    return messageHeaderName;
  }
}