/*
* 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 com.google.errorprone.annotations.CanIgnoreReturnValue;
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.
*/
@CanIgnoreReturnValue
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 <headerName>:
* <headerValue>
* @return This builder.
*/
@CanIgnoreReturnValue
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.
*/
@CanIgnoreReturnValue
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;
}
}