RtspTrackTiming.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 static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkNotNull;

import android.net.Uri;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.media3.common.C;
import androidx.media3.common.ParserException;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.UriUtil;
import androidx.media3.common.util.Util;
import com.google.common.collect.ImmutableList;

/**
 * Represents an RTSP track's timing info, included as {@link RtspHeaders#RTP_INFO} in an RTSP PLAY
 * response (RFC2326 Section 12.33).
 */
@UnstableApi
/* package */ final class RtspTrackTiming {

  /**
   * Parses the RTP-Info header into a list of {@link RtspTrackTiming RtspTrackTimings}.
   *
   * <p>The syntax of the RTP-Info (RFC2326 Section 12.33):
   *
   * <pre>
   *   RTP-Info        = "RTP-Info" ":" 1#stream-url 1*parameter
   *   stream-url      = "url" "=" url
   *   parameter       = ";" "seq" "=" 1*DIGIT
   *                   | ";" "rtptime" "=" 1*DIGIT
   * </pre>
   *
   * <p>Examples from RFC2326:
   *
   * <pre>
   *   RTP-Info:url=rtsp://foo.com/bar.file; seq=232433;rtptime=972948234
   *   RTP-Info:url=rtsp://foo.com/bar.avi/streamid=0;seq=45102,
   *            url=rtsp://foo.com/bar.avi/streamid=1;seq=30211
   * </pre>
   *
   * @param rtpInfoString The value of the RTP-Info header, with header name (RTP-Info) removed.
   * @param sessionUri The session URI, must include an {@code rtsp} scheme.
   * @return A list of parsed {@link RtspTrackTiming}.
   * @throws ParserException If parsing failed.
   */
  public static ImmutableList<RtspTrackTiming> parseTrackTiming(
      String rtpInfoString, Uri sessionUri) throws ParserException {

    ImmutableList.Builder<RtspTrackTiming> listBuilder = new ImmutableList.Builder<>();
    for (String perTrackTimingString : Util.split(rtpInfoString, ",")) {
      long rtpTime = C.TIME_UNSET;
      int sequenceNumber = C.INDEX_UNSET;
      @Nullable Uri uri = null;

      for (String attributePair : Util.split(perTrackTimingString, ";")) {
        try {
          String[] attributes = Util.splitAtFirst(attributePair, "=");
          String attributeName = attributes[0];
          String attributeValue = attributes[1];

          switch (attributeName) {
            case "url":
              uri = resolveUri(/* urlString= */ attributeValue, sessionUri);
              break;
            case "seq":
              sequenceNumber = Integer.parseInt(attributeValue);
              break;
            case "rtptime":
              rtpTime = Long.parseLong(attributeValue);
              break;
            default:
              throw ParserException.createForMalformedManifest(attributeName, /* cause= */ null);
          }
        } catch (Exception e) {
          throw ParserException.createForMalformedManifest(attributePair, e);
        }
      }

      if (uri == null
          || uri.getScheme() == null // Checks if the URI is a URL.
          || (sequenceNumber == C.INDEX_UNSET && rtpTime == C.TIME_UNSET)) {
        throw ParserException.createForMalformedManifest(perTrackTimingString, /* cause= */ null);
      }

      listBuilder.add(new RtspTrackTiming(rtpTime, sequenceNumber, uri));
    }
    return listBuilder.build();
  }

  /**
   * Resolves the input string to always be an absolute URL with RTP-Info headers
   *
   * <p>Handles some servers do not send absolute URL in RTP-Info headers. This method takes in
   * RTP-Info header's url string, and returns the correctly formatted {@link Uri url} for this
   * track. The input url string could be
   *
   * <ul>
   *   <li>A correctly formatted URL, like "{@code rtsp://foo.bar/video}".
   *   <li>A correct URI that is missing the scheme, like "{@code foo.bar/video}".
   *   <li>A path to the resource, like "{@code video}" or "{@code /video}".
   * </ul>
   *
   * @param urlString The URL included in the RTP-Info header, without the {@code url=} identifier.
   * @param sessionUri The session URI, must include an {@code rtsp} scheme, or {@link
   *     IllegalArgumentException} is thrown.
   * @return The formatted URL.
   */
  @VisibleForTesting
  /* package */ static Uri resolveUri(String urlString, Uri sessionUri) {
    checkArgument(checkNotNull(sessionUri.getScheme()).equals("rtsp"));

    Uri uri = Uri.parse(urlString);
    if (uri.isAbsolute()) {
      return uri;
    }

    // The urlString is at least missing the scheme.
    uri = Uri.parse("rtsp://" + urlString);
    String sessionUriString = sessionUri.toString();

    String host = checkNotNull(uri.getHost());
    if (host.equals(sessionUri.getHost())) {
      // Handles the case that the urlString is only missing the scheme.
      return uri;
    }

    return sessionUriString.endsWith("/")
        ? UriUtil.resolveToUri(sessionUriString, urlString)
        : UriUtil.resolveToUri(sessionUriString + "/", urlString);
  }

  /**
   * The timestamp of the next RTP packet, {@link C#TIME_UNSET} if not present.
   *
   * <p>Cannot be {@link C#TIME_UNSET} if {@link #sequenceNumber} is {@link C#INDEX_UNSET}.
   */
  public final long rtpTimestamp;
  /**
   * The sequence number of the next RTP packet, {@link C#INDEX_UNSET} if not present.
   *
   * <p>Cannot be {@link C#INDEX_UNSET} if {@link #rtpTimestamp} is {@link C#TIME_UNSET}.
   */
  public final int sequenceNumber;
  /** The {@link Uri} that identifies a matching {@link RtspMediaTrack}. */
  public final Uri uri;

  private RtspTrackTiming(long rtpTimestamp, int sequenceNumber, Uri uri) {
    this.rtpTimestamp = rtpTimestamp;
    this.sequenceNumber = sequenceNumber;
    this.uri = uri;
  }
}