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;
}
}