Publish ExoPlayer's support for RTSP.

Allow ExoPlayer to open URIs starting with rtsp://

PiperOrigin-RevId: 370653248
This commit is contained in:
claincly 2021-04-27 12:43:50 +01:00 committed by bachinger
parent 74de77a1b6
commit 8135b9c273
55 changed files with 7953 additions and 4 deletions

View File

@ -132,6 +132,8 @@
`DashMediaSource.Factory`.
* We don't currently support using platform extractors with
SmoothStreaming.
* RTSP
* Release the initial version of ExoPlayer's RTSP support.
### 2.13.3 (2021-04-14)

View File

@ -27,6 +27,7 @@ include modulePrefix + 'library-core'
include modulePrefix + 'library-dash'
include modulePrefix + 'library-extractor'
include modulePrefix + 'library-hls'
include modulePrefix + 'library-rtsp'
include modulePrefix + 'library-smoothstreaming'
include modulePrefix + 'library-transformer'
include modulePrefix + 'library-ui'
@ -55,6 +56,7 @@ project(modulePrefix + 'library-core').projectDir = new File(rootDir, 'library/c
project(modulePrefix + 'library-dash').projectDir = new File(rootDir, 'library/dash')
project(modulePrefix + 'library-extractor').projectDir = new File(rootDir, 'library/extractor')
project(modulePrefix + 'library-hls').projectDir = new File(rootDir, 'library/hls')
project(modulePrefix + 'library-rtsp').projectDir = new File(rootDir, 'library/rtsp')
project(modulePrefix + 'library-smoothstreaming').projectDir = new File(rootDir, 'library/smoothstreaming')
project(modulePrefix + 'library-transformer').projectDir = new File(rootDir, 'library/transformer')
project(modulePrefix + 'library-ui').projectDir = new File(rootDir, 'library/ui')

View File

@ -607,11 +607,11 @@ public final class C {
/**
* Represents a streaming or other media type. One of {@link #TYPE_DASH}, {@link #TYPE_SS}, {@link
* #TYPE_HLS} or {@link #TYPE_OTHER}.
* #TYPE_HLS}, {@link #TYPE_RTSP} or {@link #TYPE_OTHER}.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({TYPE_DASH, TYPE_SS, TYPE_HLS, TYPE_OTHER})
@IntDef({TYPE_DASH, TYPE_SS, TYPE_HLS, TYPE_RTSP, TYPE_OTHER})
public @interface ContentType {}
/**
* Value returned by {@link Util#inferContentType(String)} for DASH manifests.
@ -625,11 +625,13 @@ public final class C {
* Value returned by {@link Util#inferContentType(String)} for HLS manifests.
*/
public static final int TYPE_HLS = 2;
/** Value returned by {@link Util#inferContentType(String)} for RTSP. */
public static final int TYPE_RTSP = 3;
/**
* Value returned by {@link Util#inferContentType(String)} for files other than DASH, HLS or
* Smooth Streaming manifests.
* Smooth Streaming manifests, or RTSP URIs.
*/
public static final int TYPE_OTHER = 3;
public static final int TYPE_OTHER = 4;
/**
* A return value for methods where the end of an input was encountered.

View File

@ -112,6 +112,7 @@ public final class MimeTypes {
public static final String APPLICATION_EXIF = BASE_TYPE_APPLICATION + "/x-exif";
public static final String APPLICATION_ICY = BASE_TYPE_APPLICATION + "/x-icy";
public static final String APPLICATION_AIT = BASE_TYPE_APPLICATION + "/vnd.dvb.ait";
public static final String APPLICATION_RTSP = BASE_TYPE_APPLICATION + "/x-rtsp";
public static final String IMAGE_JPEG = BASE_TYPE_IMAGE + "/jpeg";

View File

@ -1800,6 +1800,11 @@ public final class Util {
*/
@ContentType
public static int inferContentType(Uri uri) {
@Nullable String scheme = uri.getScheme();
if (scheme != null && Ascii.equalsIgnoreCase("rtsp", scheme)) {
return C.TYPE_RTSP;
}
@Nullable String path = uri.getPath();
return path == null ? C.TYPE_OTHER : inferContentType(path);
}
@ -1852,6 +1857,8 @@ public final class Util {
return C.TYPE_HLS;
case MimeTypes.APPLICATION_SS:
return C.TYPE_SS;
case MimeTypes.APPLICATION_RTSP:
return C.TYPE_RTSP;
default:
return C.TYPE_OTHER;
}

View File

@ -64,3 +64,7 @@
-keepclasseswithmembers class com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory {
<init>(com.google.android.exoplayer2.upstream.DataSource$Factory);
}
-dontnote com.google.android.exoplayer2.source.rtsp.RtspMediaSource$Factory
-keepclasseswithmembers class com.google.android.exoplayer2.source.rtsp.RtspMediaSource$Factory {
<init>();
}

View File

@ -468,6 +468,14 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
} catch (Exception e) {
// Expected if the app was built without the hls module.
}
try {
Class<? extends MediaSourceFactory> factoryClazz =
Class.forName("com.google.android.exoplayer2.source.rtsp.RtspMediaSource$Factory")
.asSubclass(MediaSourceFactory.class);
factories.put(C.TYPE_RTSP, factoryClazz.getConstructor().newInstance());
} catch (Exception e) {
// Expected if the app was built without the RTSP module.
}
// LINT.ThenChange(../../../../../../../../proguard-rules.txt)
factories.put(
C.TYPE_OTHER, new ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory));

49
library/rtsp/build.gradle Normal file
View File

@ -0,0 +1,49 @@
// Copyright 2020 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.
apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
android {
buildTypes {
debug {
testCoverageEnabled = true
}
}
sourceSets.test.assets.srcDir '../../testdata/src/test/assets/'
}
dependencies {
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
implementation project(modulePrefix + 'library-core')
testImplementation project(modulePrefix + 'robolectricutils')
testImplementation project(modulePrefix + 'testutils')
testImplementation project(modulePrefix + 'testdata')
testImplementation 'org.checkerframework:checker-qual:' + checkerframeworkVersion
testImplementation 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
}
ext {
javadocTitle = 'RTSP module'
}
apply from: '../../javadoc_library.gradle'
ext {
releaseArtifact = 'exoplayer-rtsp'
releaseDescription = 'The ExoPlayer library RTSP module.'
}
apply from: '../../publish.gradle'

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2020 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.
-->
<manifest package="com.google.android.exoplayer2.source.rtsp"/>

View File

@ -0,0 +1,550 @@
/*
* 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 com.google.android.exoplayer2.source.rtsp;
import static com.google.android.exoplayer2.source.rtsp.message.RtspMessageChannel.DEFAULT_RTSP_PORT;
import static com.google.android.exoplayer2.source.rtsp.message.RtspRequest.METHOD_ANNOUNCE;
import static com.google.android.exoplayer2.source.rtsp.message.RtspRequest.METHOD_DESCRIBE;
import static com.google.android.exoplayer2.source.rtsp.message.RtspRequest.METHOD_GET_PARAMETER;
import static com.google.android.exoplayer2.source.rtsp.message.RtspRequest.METHOD_OPTIONS;
import static com.google.android.exoplayer2.source.rtsp.message.RtspRequest.METHOD_PAUSE;
import static com.google.android.exoplayer2.source.rtsp.message.RtspRequest.METHOD_PLAY;
import static com.google.android.exoplayer2.source.rtsp.message.RtspRequest.METHOD_PLAY_NOTIFY;
import static com.google.android.exoplayer2.source.rtsp.message.RtspRequest.METHOD_RECORD;
import static com.google.android.exoplayer2.source.rtsp.message.RtspRequest.METHOD_REDIRECT;
import static com.google.android.exoplayer2.source.rtsp.message.RtspRequest.METHOD_SETUP;
import static com.google.android.exoplayer2.source.rtsp.message.RtspRequest.METHOD_SET_PARAMETER;
import static com.google.android.exoplayer2.source.rtsp.message.RtspRequest.METHOD_TEARDOWN;
import static com.google.android.exoplayer2.source.rtsp.message.RtspRequest.METHOD_UNSET;
import static com.google.android.exoplayer2.util.Assertions.checkArgument;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.android.exoplayer2.util.Assertions.checkState;
import static com.google.common.base.Strings.nullToEmpty;
import android.net.Uri;
import android.os.Handler;
import android.util.SparseArray;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.source.rtsp.RtspMediaPeriod.RtpLoadInfo;
import com.google.android.exoplayer2.source.rtsp.RtspMediaSource.RtspPlaybackException;
import com.google.android.exoplayer2.source.rtsp.message.RtspDescribeResponse;
import com.google.android.exoplayer2.source.rtsp.message.RtspHeaders;
import com.google.android.exoplayer2.source.rtsp.message.RtspMessageChannel;
import com.google.android.exoplayer2.source.rtsp.message.RtspMessageUtil;
import com.google.android.exoplayer2.source.rtsp.message.RtspMessageUtil.RtspSessionHeader;
import com.google.android.exoplayer2.source.rtsp.message.RtspOptionsResponse;
import com.google.android.exoplayer2.source.rtsp.message.RtspPlayResponse;
import com.google.android.exoplayer2.source.rtsp.message.RtspRequest;
import com.google.android.exoplayer2.source.rtsp.message.RtspResponse;
import com.google.android.exoplayer2.source.rtsp.message.RtspSetupResponse;
import com.google.android.exoplayer2.source.rtsp.rtp.RtpPayloadFormat;
import com.google.android.exoplayer2.source.rtsp.sdp.MediaDescription;
import com.google.android.exoplayer2.source.rtsp.sdp.SessionDescription;
import com.google.android.exoplayer2.source.rtsp.sdp.SessionDescriptionParser;
import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.io.Closeable;
import java.io.IOException;
import java.net.Socket;
import java.util.ArrayDeque;
import java.util.List;
import java.util.Map;
import javax.net.SocketFactory;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** The RTSP client. */
/* package */ final class RtspClient implements Closeable {
private static final long DEFAULT_RTSP_KEEP_ALIVE_INTERVAL_MS = 30_000;
/** A listener for session information update. */
public interface SessionInfoListener {
/** Called when the session information is available. */
void onSessionTimelineUpdated(RtspSessionTiming timing, ImmutableList<RtspMediaTrack> tracks);
/**
* Called when failed to get session information from the RTSP server, or when error happened
* during updating the session timeline.
*/
void onSessionTimelineRequestFailed(String message, @Nullable Throwable cause);
}
/** A listener for playback events. */
public interface PlaybackEventListener {
/** Called when setup is completed and playback can start. */
void onRtspSetupCompleted();
/**
* Called when a PLAY request is acknowledged by the server and playback can start.
*
* @param startPositionUs The server-supplied start position in microseconds.
* @param trackTimingList The list of {@link RtspTrackTiming} for the playing tracks.
*/
void onPlaybackStarted(long startPositionUs, ImmutableList<RtspTrackTiming> trackTimingList);
/** Called when errors are encountered during playback. */
void onPlaybackError(RtspPlaybackException error);
}
private final SessionInfoListener sessionInfoListener;
private final Uri uri;
@Nullable private final String userAgent;
private final RtspMessageChannel messageChannel;
private final ArrayDeque<RtpLoadInfo> pendingSetupRtpLoadInfos;
// TODO(b/172331505) Add a timeout monitor for pending requests.
private final SparseArray<RtspRequest> pendingRequests;
private final MessageSender messageSender;
private @MonotonicNonNull PlaybackEventListener playbackEventListener;
private @MonotonicNonNull String sessionId;
@Nullable private KeepAliveMonitor keepAliveMonitor;
private boolean hasUpdatedTimelineAndTracks;
private long pendingSeekPositionUs;
/**
* Creates a new instance.
*
* <p>The constructor must be called on the playback thread. The thread is also where {@link
* SessionInfoListener} and {@link PlaybackEventListener} events are sent. User must {@link
* #start} the client, and {@link #close} it when done.
*
* <p>Note: all method invocations must be made from the playback thread.
*
* @param sessionInfoListener The {@link SessionInfoListener}.
* @param userAgent The user agent that will be used if needed, or {@code null} for the fallback
* to use the default user agent of the underlying platform.
* @param uri The RTSP playback URI.
*/
public RtspClient(SessionInfoListener sessionInfoListener, @Nullable String userAgent, Uri uri) {
this.sessionInfoListener = sessionInfoListener;
this.uri = RtspMessageUtil.removeUserInfo(uri);
this.userAgent = userAgent;
messageChannel = new RtspMessageChannel(new MessageListener());
pendingSetupRtpLoadInfos = new ArrayDeque<>();
pendingRequests = new SparseArray<>();
messageSender = new MessageSender();
pendingSeekPositionUs = C.TIME_UNSET;
}
/**
* Starts the client and sends an OPTIONS request.
*
* <p>Calls {@link #close()} if {@link IOException} is thrown when opening a connection to the
* supplied {@link Uri}.
*
* @throws IOException When failed to open a connection to the supplied {@link Uri}.
*/
public void start() throws IOException {
checkArgument(uri.getHost() != null);
int rtspPort = uri.getPort() > 0 ? uri.getPort() : DEFAULT_RTSP_PORT;
Socket socket = SocketFactory.getDefault().createSocket(checkNotNull(uri.getHost()), rtspPort);
try {
messageChannel.openSocket(socket);
} catch (IOException e) {
Util.closeQuietly(messageChannel);
throw e;
}
messageSender.sendOptionsRequest(uri, sessionId);
}
/** Sets the {@link PlaybackEventListener} to receive playback events. */
public void setPlaybackEventListener(PlaybackEventListener playbackEventListener) {
this.playbackEventListener = playbackEventListener;
}
/**
* Triggers RTSP SETUP requests after track selection.
*
* <p>A {@link PlaybackEventListener} must be set via {@link #setPlaybackEventListener} before
* calling this method. All selected tracks (represented by {@link RtpLoadInfo}) must have valid
* transport.
*
* @param loadInfos A list of selected tracks represented by {@link RtpLoadInfo}.
*/
public void setupSelectedTracks(List<RtpLoadInfo> loadInfos) {
pendingSetupRtpLoadInfos.addAll(loadInfos);
continueSetupRtspTrack();
}
/**
* Starts RTSP playback by sending RTSP PLAY request.
*
* @param offsetMs The playback offset in milliseconds, with respect to the stream start position.
*/
public void startPlayback(long offsetMs) {
messageSender.sendPlayRequest(uri, offsetMs, checkNotNull(sessionId));
}
/**
* Seeks to a specific time using RTSP.
*
* <p>Call this method only when in-buffer seek is not feasible. An RTSP PAUSE, and an RTSP PLAY
* request will be sent out to perform a seek on the server side.
*
* @param positionUs The seek time measured in microseconds.
*/
public void seekToUs(long positionUs) {
messageSender.sendPauseRequest(uri, checkNotNull(sessionId));
pendingSeekPositionUs = positionUs;
}
@Override
public void close() throws IOException {
if (keepAliveMonitor != null) {
// Playback has started. We have to stop the periodic keep alive and send a TEARDOWN so that
// the RTSP server stops sending RTP packets and frees up resources.
keepAliveMonitor.close();
keepAliveMonitor = null;
messageSender.sendTeardownRequest(uri, checkNotNull(sessionId));
}
messageChannel.close();
}
private void continueSetupRtspTrack() {
@Nullable RtpLoadInfo loadInfo = pendingSetupRtpLoadInfos.pollFirst();
if (loadInfo == null) {
checkNotNull(playbackEventListener).onRtspSetupCompleted();
return;
}
messageSender.sendSetupRequest(loadInfo.getTrackUri(), loadInfo.getTransport(), sessionId);
}
/**
* Returns whether the RTSP server supports the DESCRIBE method.
*
* <p>The DESCRIBE method is marked "recommended to implement" in RFC2326 Section 10. We assume
* the server supports DESCRIBE, if the OPTIONS response does not include a PUBLIC header.
*
* @param serverSupportedMethods A list of RTSP methods (as defined in RFC2326 Section 10, encoded
* as {@link RtspRequest.Method}) that are supported by the RTSP server.
*/
private static boolean serverSupportsDescribe(List<Integer> serverSupportedMethods) {
return serverSupportedMethods.isEmpty() || serverSupportedMethods.contains(METHOD_DESCRIBE);
}
/**
* Gets the included {@link RtspMediaTrack RtspMediaTracks} from a {@link SessionDescription}.
*
* @param sessionDescription The {@link SessionDescription}.
* @param uri The RTSP playback URI.
*/
private static ImmutableList<RtspMediaTrack> buildTrackList(
SessionDescription sessionDescription, Uri uri) {
ImmutableList.Builder<RtspMediaTrack> trackListBuilder = new ImmutableList.Builder<>();
for (int i = 0; i < sessionDescription.mediaDescriptionList.size(); i++) {
MediaDescription mediaDescription = sessionDescription.mediaDescriptionList.get(i);
// Includes tracks with supported formats only.
if (RtpPayloadFormat.isFormatSupported(mediaDescription)) {
trackListBuilder.add(new RtspMediaTrack(mediaDescription, uri));
}
}
return trackListBuilder.build();
}
private final class MessageSender {
private int cSeq;
public void sendOptionsRequest(Uri uri, @Nullable String sessionId) {
sendRequest(
getRequestWithCommonHeaders(
METHOD_OPTIONS, sessionId, /* additionalHeaders= */ ImmutableMap.of(), uri));
}
public void sendDescribeRequest(Uri uri, @Nullable String sessionId) {
sendRequest(
getRequestWithCommonHeaders(
METHOD_DESCRIBE, sessionId, /* additionalHeaders= */ ImmutableMap.of(), uri));
}
public void sendSetupRequest(Uri trackUri, String transport, @Nullable String sessionId) {
sendRequest(
getRequestWithCommonHeaders(
METHOD_SETUP,
sessionId,
/* additionalHeaders= */ ImmutableMap.of(RtspHeaders.TRANSPORT, transport),
trackUri));
}
public void sendPlayRequest(Uri uri, long offsetMs, String sessionId) {
sendRequest(
getRequestWithCommonHeaders(
METHOD_PLAY,
sessionId,
/* additionalHeaders= */ ImmutableMap.of(
RtspHeaders.RANGE, RtspSessionTiming.getOffsetStartTimeTiming(offsetMs)),
uri));
}
public void sendTeardownRequest(Uri uri, String sessionId) {
sendRequest(
getRequestWithCommonHeaders(
METHOD_TEARDOWN, sessionId, /* additionalHeaders= */ ImmutableMap.of(), uri));
}
public void sendPauseRequest(Uri uri, String sessionId) {
sendRequest(
getRequestWithCommonHeaders(
METHOD_PAUSE, sessionId, /* additionalHeaders= */ ImmutableMap.of(), uri));
}
private RtspRequest getRequestWithCommonHeaders(
@RtspRequest.Method int method,
@Nullable String sessionId,
Map<String, String> additionalHeaders,
Uri uri) {
RtspHeaders.Builder headersBuilder = new RtspHeaders.Builder();
headersBuilder.add(RtspHeaders.CSEQ, String.valueOf(cSeq++));
if (userAgent != null) {
headersBuilder.add(RtspHeaders.USER_AGENT, userAgent);
}
if (sessionId != null) {
headersBuilder.add(RtspHeaders.SESSION, sessionId);
}
headersBuilder.addAll(additionalHeaders);
return new RtspRequest(uri, method, headersBuilder.build(), /* messageBody= */ "");
}
private void sendRequest(RtspRequest request) {
int cSeq = Integer.parseInt(checkNotNull(request.headers.get(RtspHeaders.CSEQ)));
checkState(pendingRequests.get(cSeq) == null);
pendingRequests.append(cSeq, request);
messageChannel.send(RtspMessageUtil.serializeRequest(request));
}
}
private final class MessageListener implements RtspMessageChannel.MessageListener {
@Override
public void onRtspMessageReceived(List<String> message) {
RtspResponse response = RtspMessageUtil.parseResponse(message);
int cSeq = Integer.parseInt(checkNotNull(response.headers.get(RtspHeaders.CSEQ)));
@Nullable RtspRequest matchingRequest = pendingRequests.get(cSeq);
if (matchingRequest == null) {
return;
} else {
pendingRequests.remove(cSeq);
}
@RtspRequest.Method int requestMethod = matchingRequest.method;
if (response.status != 200) {
dispatchRtspError(
new RtspPlaybackException(
RtspMessageUtil.toMethodString(requestMethod) + " " + response.status));
return;
}
try {
switch (requestMethod) {
case METHOD_OPTIONS:
onOptionsResponseReceived(
new RtspOptionsResponse(
response.status,
RtspMessageUtil.parsePublicHeader(response.headers.get(RtspHeaders.PUBLIC))));
break;
case METHOD_DESCRIBE:
onDescribeResponseReceived(
new RtspDescribeResponse(
response.status, SessionDescriptionParser.parse(response.messageBody)));
break;
case METHOD_SETUP:
@Nullable String sessionHeaderString = response.headers.get(RtspHeaders.SESSION);
@Nullable String transportHeaderString = response.headers.get(RtspHeaders.TRANSPORT);
if (sessionHeaderString == null || transportHeaderString == null) {
throw new ParserException();
}
RtspSessionHeader sessionHeader =
RtspMessageUtil.parseSessionHeader(sessionHeaderString);
onSetupResponseReceived(
new RtspSetupResponse(response.status, sessionHeader, transportHeaderString));
break;
case METHOD_PLAY:
// Range header is optional for a PLAY response (RFC2326 Section 12).
@Nullable String startTimingString = response.headers.get(RtspHeaders.RANGE);
RtspSessionTiming timing =
startTimingString == null
? RtspSessionTiming.DEFAULT
: RtspSessionTiming.parseTiming(startTimingString);
@Nullable String rtpInfoString = response.headers.get(RtspHeaders.RTP_INFO);
ImmutableList<RtspTrackTiming> trackTimingList =
rtpInfoString == null
? ImmutableList.of()
: RtspTrackTiming.parseTrackTiming(rtpInfoString);
onPlayResponseReceived(new RtspPlayResponse(response.status, timing, trackTimingList));
break;
case METHOD_GET_PARAMETER:
onGetParameterResponseReceived(response);
break;
case METHOD_TEARDOWN:
onTeardownResponseReceived(response);
break;
case METHOD_PAUSE:
onPauseResponseReceived(response);
break;
case METHOD_PLAY_NOTIFY:
case METHOD_RECORD:
case METHOD_REDIRECT:
case METHOD_ANNOUNCE:
case METHOD_SET_PARAMETER:
onUnsupportedResponseReceived(response);
break;
case METHOD_UNSET:
default:
throw new IllegalStateException();
}
} catch (ParserException e) {
dispatchRtspError(new RtspPlaybackException(e));
}
}
// Response handlers must only be called only on 200 (OK) responses.
public void onOptionsResponseReceived(RtspOptionsResponse response) {
if (keepAliveMonitor != null) {
// Ignores the OPTIONS requests that are sent to keep RTSP connection alive.
return;
}
if (serverSupportsDescribe(response.supportedMethods)) {
messageSender.sendDescribeRequest(uri, sessionId);
} else {
sessionInfoListener.onSessionTimelineRequestFailed(
"DESCRIBE not supported.", /* cause= */ null);
}
}
public void onDescribeResponseReceived(RtspDescribeResponse response) {
String sessionRangeAttributeString =
checkNotNull(response.sessionDescription.attributes.get(SessionDescription.ATTR_RANGE));
try {
sessionInfoListener.onSessionTimelineUpdated(
RtspSessionTiming.parseTiming(sessionRangeAttributeString),
buildTrackList(response.sessionDescription, uri));
hasUpdatedTimelineAndTracks = true;
} catch (ParserException e) {
sessionInfoListener.onSessionTimelineRequestFailed("SDP format error.", /* cause= */ e);
}
}
public void onSetupResponseReceived(RtspSetupResponse response) {
sessionId = response.sessionHeader.sessionId;
continueSetupRtspTrack();
}
public void onPlayResponseReceived(RtspPlayResponse response) {
if (keepAliveMonitor == null) {
keepAliveMonitor = new KeepAliveMonitor(DEFAULT_RTSP_KEEP_ALIVE_INTERVAL_MS);
keepAliveMonitor.start();
}
checkNotNull(playbackEventListener)
.onPlaybackStarted(
C.msToUs(response.sessionTiming.startTimeMs), response.trackTimingList);
pendingSeekPositionUs = C.TIME_UNSET;
}
public void onPauseResponseReceived(RtspResponse response) {
if (pendingSeekPositionUs != C.TIME_UNSET) {
startPlayback(C.usToMs(pendingSeekPositionUs));
}
}
public void onGetParameterResponseReceived(RtspResponse response) {
// Do nothing.
}
public void onTeardownResponseReceived(RtspResponse response) {
// Do nothing.
}
public void onUnsupportedResponseReceived(RtspResponse response) {
// Do nothing.
}
private void dispatchRtspError(Throwable error) {
RtspPlaybackException playbackException =
error instanceof RtspPlaybackException
? (RtspPlaybackException) error
: new RtspPlaybackException(error);
if (hasUpdatedTimelineAndTracks) {
// Playback event listener must be non-null after timeline has been updated.
checkNotNull(playbackEventListener).onPlaybackError(playbackException);
} else {
sessionInfoListener.onSessionTimelineRequestFailed(nullToEmpty(error.getMessage()), error);
}
}
}
/** Sends periodic OPTIONS requests to keep RTSP connection alive. */
private final class KeepAliveMonitor implements Runnable, Closeable {
private final Handler keepAliveHandler;
private final long intervalMs;
private boolean isStarted;
/**
* Creates a new instance.
*
* <p>Constructor must be invoked on the playback thread.
*
* @param intervalMs The time between consecutive RTSP keep-alive requests, in milliseconds.
*/
public KeepAliveMonitor(long intervalMs) {
this.intervalMs = intervalMs;
keepAliveHandler = Util.createHandlerForCurrentLooper();
}
/** Starts Keep-alive. */
public void start() {
if (isStarted) {
return;
}
isStarted = true;
keepAliveHandler.postDelayed(this, intervalMs);
}
@Override
public void run() {
messageSender.sendOptionsRequest(uri, sessionId);
keepAliveHandler.postDelayed(this, intervalMs);
}
@Override
public void close() {
isStarted = false;
keepAliveHandler.removeCallbacks(this);
}
}
}

View File

@ -0,0 +1,658 @@
/*
* 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 com.google.android.exoplayer2.source.rtsp;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.android.exoplayer2.util.Assertions.checkState;
import static com.google.android.exoplayer2.util.Assertions.checkStateNotNull;
import static java.lang.Math.min;
import android.net.Uri;
import android.os.Handler;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.FormatHolder;
import com.google.android.exoplayer2.SeekParameters;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.SampleQueue;
import com.google.android.exoplayer2.source.SampleQueue.UpstreamFormatChangedListener;
import com.google.android.exoplayer2.source.SampleStream;
import com.google.android.exoplayer2.source.SampleStream.ReadDataResult;
import com.google.android.exoplayer2.source.SampleStream.ReadFlags;
import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.source.rtsp.RtspClient.PlaybackEventListener;
import com.google.android.exoplayer2.source.rtsp.RtspMediaSource.RtspPlaybackException;
import com.google.android.exoplayer2.source.rtsp.rtp.RtpDataLoadable;
import com.google.android.exoplayer2.trackselection.ExoTrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.Loader;
import com.google.android.exoplayer2.upstream.Loader.Loadable;
import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableList;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.util.ArrayList;
import java.util.List;
import org.checkerframework.checker.nullness.compatqual.NullableType;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** A {@link MediaPeriod} that loads an RTSP stream. */
public final class RtspMediaPeriod implements MediaPeriod {
private static final String TAG = "RtspMediaPeriod";
private final Allocator allocator;
private final Handler handler;
private final InternalListener internalListener;
private final RtspClient rtspClient;
private final List<RtspLoaderWrapper> rtspLoaderWrappers;
private final List<RtpLoadInfo> selectedLoadInfos;
private @MonotonicNonNull Callback callback;
private @MonotonicNonNull ImmutableList<TrackGroup> trackGroups;
@Nullable private IOException preparationError;
@Nullable private RtspPlaybackException playbackException;
private long pendingSeekPositionUs;
private boolean loadingFinished;
private boolean released;
private boolean prepared;
private boolean trackSelected;
/**
* Creates an RTSP media period.
*
* @param allocator An {@link Allocator} from which to obtain media buffer allocations.
* @param rtspTracks A list of tracks in an RTSP playback session.
* @param rtspClient The {@link RtspClient} for the current RTSP playback.
*/
public RtspMediaPeriod(
Allocator allocator, List<RtspMediaTrack> rtspTracks, RtspClient rtspClient) {
this.allocator = allocator;
handler = Util.createHandlerForCurrentLooper();
internalListener = new InternalListener();
rtspLoaderWrappers = new ArrayList<>(rtspTracks.size());
this.rtspClient = rtspClient;
this.rtspClient.setPlaybackEventListener(internalListener);
for (int i = 0; i < rtspTracks.size(); i++) {
RtspMediaTrack rtspMediaTrack = rtspTracks.get(i);
rtspLoaderWrappers.add(new RtspLoaderWrapper(rtspMediaTrack, /* trackId= */ i));
}
selectedLoadInfos = new ArrayList<>(rtspTracks.size());
pendingSeekPositionUs = C.TIME_UNSET;
}
/** Releases the {@link RtspMediaPeriod}. */
public void release() {
for (int i = 0; i < rtspLoaderWrappers.size(); i++) {
rtspLoaderWrappers.get(i).release();
}
released = true;
}
@Override
public void prepare(Callback callback, long positionUs) {
this.callback = callback;
for (int i = 0; i < rtspLoaderWrappers.size(); i++) {
rtspLoaderWrappers.get(i).startLoading();
}
}
@Override
public void maybeThrowPrepareError() throws IOException {
if (preparationError != null) {
throw preparationError;
}
}
@Override
public TrackGroupArray getTrackGroups() {
checkState(prepared);
return new TrackGroupArray(checkNotNull(trackGroups).toArray(new TrackGroup[0]));
}
@Override
public ImmutableList<StreamKey> getStreamKeys(List<ExoTrackSelection> trackSelections) {
return ImmutableList.of();
}
@Override
public long selectTracks(
@NullableType ExoTrackSelection[] selections,
boolean[] mayRetainStreamFlags,
@NullableType SampleStream[] streams,
boolean[] streamResetFlags,
long positionUs) {
// Deselect old tracks.
// Input array streams contains the streams selected in the previous track selection.
for (int i = 0; i < selections.length; i++) {
if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) {
streams[i] = null;
}
}
// Select new tracks.
selectedLoadInfos.clear();
for (int i = 0; i < selections.length; i++) {
TrackSelection selection = selections[i];
if (selection == null) {
continue;
}
TrackGroup trackGroup = selection.getTrackGroup();
int trackGroupIndex = checkNotNull(trackGroups).indexOf(trackGroup);
selectedLoadInfos.add(checkNotNull(rtspLoaderWrappers.get(trackGroupIndex)).loadInfo);
// Find the sampleStreamWrapper that contains this track group.
if (trackGroups.contains(trackGroup)) {
if (streams[i] == null) {
streams[i] = new SampleStreamImpl(trackGroupIndex);
// Update flag for newly created SampleStream.
streamResetFlags[i] = true;
}
}
}
// Cancel non-selected loadables.
for (int i = 0; i < rtspLoaderWrappers.size(); i++) {
RtspLoaderWrapper loadControl = rtspLoaderWrappers.get(i);
if (!selectedLoadInfos.contains(loadControl.loadInfo)) {
loadControl.cancelLoad();
}
}
trackSelected = true;
maybeSetupTracks();
return positionUs;
}
@Override
public void discardBuffer(long positionUs, boolean toKeyframe) {
if (isSeekPending()) {
return;
}
for (int i = 0; i < rtspLoaderWrappers.size(); i++) {
RtspLoaderWrapper loaderWrapper = rtspLoaderWrappers.get(i);
if (!loaderWrapper.canceled) {
loaderWrapper.sampleQueue.discardTo(positionUs, toKeyframe, /* stopAtReadPosition= */ true);
}
}
}
@Override
public long readDiscontinuity() {
return C.TIME_UNSET;
}
@Override
public long seekToUs(long positionUs) {
if (isSeekPending()) {
// TODO(internal b/172331505) Allow seek when a seek is pending.
// Does not allow another seek if a seek is pending.
return pendingSeekPositionUs;
}
if (seekInsideBufferUs(positionUs)) {
return positionUs;
}
pendingSeekPositionUs = positionUs;
rtspClient.seekToUs(positionUs);
for (int i = 0; i < rtspLoaderWrappers.size(); i++) {
rtspLoaderWrappers.get(i).seekTo(positionUs);
}
return positionUs;
}
@Override
public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {
return positionUs;
}
@Override
public long getBufferedPositionUs() {
if (loadingFinished || rtspLoaderWrappers.isEmpty()) {
return C.TIME_END_OF_SOURCE;
}
if (isSeekPending()) {
return pendingSeekPositionUs;
}
long bufferedPositionUs = rtspLoaderWrappers.get(0).sampleQueue.getLargestQueuedTimestampUs();
for (int i = 1; i < rtspLoaderWrappers.size(); i++) {
bufferedPositionUs =
min(
bufferedPositionUs,
checkNotNull(rtspLoaderWrappers.get(i)).sampleQueue.getLargestQueuedTimestampUs());
}
return bufferedPositionUs;
}
@Override
public long getNextLoadPositionUs() {
return getBufferedPositionUs();
}
@Override
public boolean continueLoading(long positionUs) {
return isLoading();
}
@Override
public boolean isLoading() {
return !loadingFinished;
}
@Override
public void reevaluateBuffer(long positionUs) {
// Do nothing.
}
// SampleStream methods.
/* package */ boolean isReady(int trackGroupIndex) {
return rtspLoaderWrappers.get(trackGroupIndex).isSampleQueueReady();
}
@ReadDataResult
/* package */ int readData(
int sampleQueueIndex,
FormatHolder formatHolder,
DecoderInputBuffer buffer,
@ReadFlags int readFlags) {
return rtspLoaderWrappers.get(sampleQueueIndex).read(formatHolder, buffer, readFlags);
}
// Internal methods.
@Nullable
private RtpDataLoadable getLoadableByTrackUri(Uri trackUri) {
for (int i = 0; i < rtspLoaderWrappers.size(); i++) {
RtpLoadInfo loadInfo = rtspLoaderWrappers.get(i).loadInfo;
if (loadInfo.getTrackUri().equals(trackUri)) {
return loadInfo.loadable;
}
}
return null;
}
private boolean isSeekPending() {
return pendingSeekPositionUs != C.TIME_UNSET;
}
private void maybeFinishPrepare() {
if (released || prepared) {
return;
}
// Make sure all sample queues have got format assigned.
for (int i = 0; i < rtspLoaderWrappers.size(); i++) {
if (rtspLoaderWrappers.get(i).sampleQueue.getUpstreamFormat() == null) {
return;
}
}
prepared = true;
trackGroups = buildTrackGroups(ImmutableList.copyOf(rtspLoaderWrappers));
checkNotNull(callback).onPrepared(/* mediaPeriod= */ this);
}
/**
* Attempts to seek to the specified position within the sample queues.
*
* @param positionUs The seek position in microseconds.
* @return Whether the in-buffer seek was successful for all loading RTSP tracks.
*/
private boolean seekInsideBufferUs(long positionUs) {
for (int i = 0; i < rtspLoaderWrappers.size(); i++) {
SampleQueue sampleQueue = rtspLoaderWrappers.get(i).sampleQueue;
if (!sampleQueue.seekTo(positionUs, /* allowTimeBeyondBuffer= */ false)) {
return false;
}
}
return true;
}
private void maybeSetupTracks() {
boolean transportReady = true;
for (int i = 0; i < selectedLoadInfos.size(); i++) {
transportReady &= selectedLoadInfos.get(i).isTransportReady();
}
if (transportReady && trackSelected) {
rtspClient.setupSelectedTracks(selectedLoadInfos);
}
}
private static ImmutableList<TrackGroup> buildTrackGroups(
ImmutableList<RtspLoaderWrapper> rtspLoaderWrappers) {
ImmutableList.Builder<TrackGroup> listBuilder = new ImmutableList.Builder<>();
SampleQueue sampleQueue;
for (int i = 0; i < rtspLoaderWrappers.size(); i++) {
sampleQueue = rtspLoaderWrappers.get(i).sampleQueue;
listBuilder.add(new TrackGroup(checkNotNull(sampleQueue.getUpstreamFormat())));
}
return listBuilder.build();
}
private final class InternalListener
implements ExtractorOutput,
Loader.Callback<RtpDataLoadable>,
UpstreamFormatChangedListener,
PlaybackEventListener {
// ExtractorOutput implementation.
@Override
public TrackOutput track(int id, int type) {
return checkNotNull(rtspLoaderWrappers.get(id)).sampleQueue;
}
@Override
public void endTracks() {
// TODO(b/172331505) Implement this method.
}
@Override
public void seekMap(SeekMap seekMap) {
// TODO(b/172331505) Implement this method.
}
// Loadable.Callback implementation.
@Override
public void onLoadCompleted(
RtpDataLoadable loadable, long elapsedRealtimeMs, long loadDurationMs) {}
@Override
public void onLoadCanceled(
RtpDataLoadable loadable, long elapsedRealtimeMs, long loadDurationMs, boolean released) {}
@Override
public Loader.LoadErrorAction onLoadError(
RtpDataLoadable loadable,
long elapsedRealtimeMs,
long loadDurationMs,
IOException error,
int errorCount) {
/* TODO(b/172331505) Sort out the retry policy.
Three cases for IOException:
- Socket open failure for RTP or RTCP.
- RETRY for the RTCP open failure.
- ExtractorInput read IOException (socket timeout, etc)
- Keep retrying unless playback is stopped.
- RtpPayloadReader consume ParserException (mal-formatted RTP packet)
- Don't retry? (if a packet is distorted on the fly, the packet is likely discarded by the
system, i.e. the server's sent a mal-formatted packet).
*/
if (!prepared) {
preparationError = error;
} else {
if (error.getCause() instanceof SocketTimeoutException) {
handleSocketTimeout(loadable);
} else {
playbackException =
new RtspPlaybackException(
/* message= */ loadable.rtspMediaTrack.uri.toString(), error);
}
}
return Loader.DONT_RETRY;
}
// SampleQueue.UpstreamFormatChangedListener implementation.
@Override
public void onUpstreamFormatChanged(Format format) {
handler.post(RtspMediaPeriod.this::maybeFinishPrepare);
}
// RtspClient.PlaybackEventListener implementation.
@Override
public void onRtspSetupCompleted() {
rtspClient.startPlayback(/* offsetMs= */ 0);
}
@Override
public void onPlaybackStarted(
long startPositionUs, ImmutableList<RtspTrackTiming> trackTimingList) {
// Validate that the trackTimingList contains timings for the selected tracks.
ArrayList<Uri> trackUrisWithTiming = new ArrayList<>(trackTimingList.size());
for (int i = 0; i < trackTimingList.size(); i++) {
trackUrisWithTiming.add(trackTimingList.get(i).uri);
}
for (int i = 0; i < selectedLoadInfos.size(); i++) {
RtpLoadInfo loadInfo = selectedLoadInfos.get(i);
if (!trackUrisWithTiming.contains(loadInfo.getTrackUri())) {
playbackException =
new RtspPlaybackException(
"Server did not provide timing for track " + loadInfo.getTrackUri());
return;
}
}
for (int i = 0; i < trackTimingList.size(); i++) {
RtspTrackTiming trackTiming = trackTimingList.get(i);
@Nullable RtpDataLoadable dataLoadable = getLoadableByTrackUri(trackTiming.uri);
if (dataLoadable == null) {
continue;
}
dataLoadable.setTimestamp(trackTiming.rtpTimestamp);
dataLoadable.setSequenceNumber(trackTiming.sequenceNumber);
if (isSeekPending()) {
dataLoadable.seekToUs(startPositionUs, trackTiming.rtpTimestamp);
}
}
if (isSeekPending()) {
pendingSeekPositionUs = C.TIME_UNSET;
}
}
@Override
public void onPlaybackError(RtspPlaybackException error) {
playbackException = error;
}
/** Handles the {@link Loadable} whose {@link DataSource} timed out. */
private void handleSocketTimeout(RtpDataLoadable loadable) {
// TODO(b/172331505) Allow for retry when loading is not ending.
if (getBufferedPositionUs() == Long.MIN_VALUE) {
// Raise exception if no sample has been received so far.
playbackException = new RtspPlaybackException("Possible dropped UDP connection.");
return;
}
for (int i = 0; i < rtspLoaderWrappers.size(); i++) {
RtspLoaderWrapper loaderWrapper = rtspLoaderWrappers.get(i);
if (loaderWrapper.loadInfo.loadable == loadable) {
loaderWrapper.cancelLoad();
return;
}
}
playbackException = new RtspPlaybackException("Unknown loadable timed out.");
}
}
private final class SampleStreamImpl implements SampleStream {
private final int track;
public SampleStreamImpl(int track) {
this.track = track;
}
@Override
public boolean isReady() {
return RtspMediaPeriod.this.isReady(track);
}
@Override
public void maybeThrowError() throws RtspPlaybackException {
if (playbackException != null) {
throw playbackException;
}
}
@Override
public int readData(
FormatHolder formatHolder, DecoderInputBuffer buffer, @ReadFlags int readFlags) {
return RtspMediaPeriod.this.readData(track, formatHolder, buffer, readFlags);
}
@Override
public int skipData(long positionUs) {
return 0;
}
}
/** Manages the loading of an RTSP track. */
private final class RtspLoaderWrapper {
/** The {@link RtpLoadInfo} of the RTSP track to load. */
public final RtpLoadInfo loadInfo;
private final Loader loader;
private final SampleQueue sampleQueue;
private boolean canceled;
private boolean released;
/**
* Creates a new instance.
*
* <p>Instances must be {@link #release() released} after loadings conclude.
*/
public RtspLoaderWrapper(RtspMediaTrack mediaTrack, int trackId) {
loadInfo = new RtpLoadInfo(mediaTrack, trackId);
loader = new Loader("ExoPlayer:RtspMediaPeriod:RtspDataLoader " + trackId);
sampleQueue = SampleQueue.createWithoutDrm(allocator);
sampleQueue.setUpstreamFormatChangeListener(internalListener);
}
/** Starts loading. */
public void startLoading() {
loader.startLoading(
loadInfo.loadable, /* callback= */ internalListener, /* defaultMinRetryCount= */ 0);
}
public boolean isSampleQueueReady() {
return sampleQueue.isReady(/* loadingFinished= */ canceled);
}
@ReadDataResult
public int read(
FormatHolder formatHolder, DecoderInputBuffer buffer, @ReadFlags int readFlags) {
return sampleQueue.read(formatHolder, buffer, readFlags, /* loadingFinished= */ canceled);
}
/** Cancels loading. */
public void cancelLoad() {
if (canceled) {
return;
}
loadInfo.loadable.cancelLoad();
canceled = true;
// Update loadingFinished every time loading is canceled.
for (int i = 0; i < rtspLoaderWrappers.size(); i++) {
loadingFinished &= rtspLoaderWrappers.get(i).canceled;
}
}
/** Resets the {@link Loadable} and {@link SampleQueue} to prepare for an RTSP seek. */
public void seekTo(long positionUs) {
loadInfo.loadable.resetForSeek();
sampleQueue.reset();
sampleQueue.setStartTimeUs(positionUs);
}
/** Releases the instance. */
public void release() {
if (released) {
return;
}
loader.release();
sampleQueue.release();
released = true;
}
}
/** Groups the info needed for loading one RTSP track in RTP. */
/* package */ final class RtpLoadInfo {
/** The {@link RtspMediaTrack}. */
public final RtspMediaTrack mediaTrack;
private final RtpDataLoadable loadable;
@Nullable private String transport;
/** Creates a new instance. */
public RtpLoadInfo(RtspMediaTrack mediaTrack, int trackId) {
this.mediaTrack = mediaTrack;
RtpDataLoadable.EventListener transportEventListener =
(transport) -> {
this.transport = transport;
maybeSetupTracks();
};
this.loadable =
new RtpDataLoadable(
trackId,
mediaTrack,
/* eventListener= */ transportEventListener,
/* output= */ internalListener);
}
/**
* Returns whether RTP transport is ready. Call {@link #getTransport()} only after transport is
* ready.
*/
public boolean isTransportReady() {
return transport != null;
}
/**
* Gets the transport string for RTP loading.
*
* @throws IllegalStateException When transport for this RTP stream is not set.
*/
public String getTransport() {
checkStateNotNull(transport);
return transport;
}
/** Gets the {@link Uri} for the loading RTSP track. */
public Uri getTrackUri() {
return loadable.rtspMediaTrack.uri;
}
}
}

View File

@ -0,0 +1,209 @@
/*
* 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 com.google.android.exoplayer2.source.rtsp;
import static com.google.android.exoplayer2.ExoPlayerLibraryInfo.VERSION_SLASHY;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.android.exoplayer2.util.Util.castNonNull;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.DrmSessionManagerProvider;
import com.google.android.exoplayer2.source.BaseMediaSource;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MediaSourceFactory;
import com.google.android.exoplayer2.source.SinglePeriodTimeline;
import com.google.android.exoplayer2.source.rtsp.RtspClient.SessionInfoListener;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableList;
import java.io.IOException;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** An Rtsp {@link MediaSource} */
public final class RtspMediaSource extends BaseMediaSource {
/**
* Factory for {@link RtspMediaSource}
*
* <p>This factory doesn't support the following methods from {@link MediaSourceFactory}:
*
* <ul>
* <li>{@link #setDrmSessionManagerProvider(DrmSessionManagerProvider)}
* <li>{@link #setDrmSessionManager(DrmSessionManager)}
* <li>{@link #setDrmHttpDataSourceFactory(HttpDataSource.Factory)}
* <li>{@link #setDrmUserAgent(String)}
* <li>{@link #setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy)}
* </ul>
*/
public static final class Factory implements MediaSourceFactory {
/** @deprecated Not supported. */
@Deprecated
@Override
public Factory setDrmSessionManagerProvider(
@Nullable DrmSessionManagerProvider drmSessionManager) {
return this;
}
/** @deprecated Not supported. */
@Deprecated
@Override
public Factory setDrmSessionManager(@Nullable DrmSessionManager drmSessionManager) {
return this;
}
/** @deprecated Not supported. */
@Deprecated
@Override
public Factory setDrmHttpDataSourceFactory(
@Nullable HttpDataSource.Factory drmHttpDataSourceFactory) {
return this;
}
/** @deprecated Not supported. */
@Deprecated
@Override
public Factory setDrmUserAgent(@Nullable String userAgent) {
return this;
}
/** @deprecated Not supported. */
@Deprecated
@Override
public Factory setLoadErrorHandlingPolicy(
@Nullable LoadErrorHandlingPolicy loadErrorHandlingPolicy) {
return this;
}
@Override
public int[] getSupportedTypes() {
return new int[] {C.TYPE_RTSP};
}
/**
* Returns a new {@link RtspMediaSource} using the current parameters.
*
* @param mediaItem The {@link MediaItem}.
* @return The new {@link RtspMediaSource}.
* @throws NullPointerException if {@link MediaItem#playbackProperties} is {@code null}.
*/
@Override
public RtspMediaSource createMediaSource(MediaItem mediaItem) {
checkNotNull(mediaItem.playbackProperties);
return new RtspMediaSource(mediaItem);
}
}
/** Thrown when an exception or error is encountered during loading an RTSP stream. */
public static final class RtspPlaybackException extends IOException {
public RtspPlaybackException(String message) {
super(message);
}
public RtspPlaybackException(Throwable e) {
super(e);
}
public RtspPlaybackException(String message, Throwable e) {
super(message, e);
}
}
private final MediaItem mediaItem;
private @MonotonicNonNull RtspClient rtspClient;
@Nullable private ImmutableList<RtspMediaTrack> rtspMediaTracks;
@Nullable private IOException sourcePrepareException;
private RtspMediaSource(MediaItem mediaItem) {
this.mediaItem = mediaItem;
}
@Override
protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
checkNotNull(mediaItem.playbackProperties);
try {
rtspClient =
new RtspClient(
new SessionInfoListenerImpl(),
/* userAgent= */ VERSION_SLASHY,
mediaItem.playbackProperties.uri);
rtspClient.start();
} catch (IOException e) {
sourcePrepareException = new RtspPlaybackException("RtspClient not opened.", e);
}
}
@Override
protected void releaseSourceInternal() {
Util.closeQuietly(rtspClient);
}
@Override
public MediaItem getMediaItem() {
return mediaItem;
}
@Override
public void maybeThrowSourceInfoRefreshError() throws IOException {
if (sourcePrepareException != null) {
throw sourcePrepareException;
}
}
@Override
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
return new RtspMediaPeriod(allocator, checkNotNull(rtspMediaTracks), checkNotNull(rtspClient));
}
@Override
public void releasePeriod(MediaPeriod mediaPeriod) {
((RtspMediaPeriod) mediaPeriod).release();
}
private final class SessionInfoListenerImpl implements SessionInfoListener {
@Override
public void onSessionTimelineUpdated(
RtspSessionTiming timing, ImmutableList<RtspMediaTrack> tracks) {
rtspMediaTracks = tracks;
refreshSourceInfo(
new SinglePeriodTimeline(
/* durationUs= */ C.msToUs(timing.getDurationMs()),
/* isSeekable= */ !timing.isLive(),
/* isDynamic= */ false,
/* useLiveConfiguration= */ timing.isLive(),
/* manifest= */ null,
mediaItem));
}
@Override
public void onSessionTimelineRequestFailed(String message, @Nullable Throwable cause) {
if (cause == null) {
sourcePrepareException = new RtspPlaybackException(message);
} else {
sourcePrepareException = new RtspPlaybackException(message, castNonNull(cause));
}
}
}
}

View File

@ -0,0 +1,218 @@
/*
* 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 com.google.android.exoplayer2.source.rtsp;
import static com.google.android.exoplayer2.source.rtsp.rtp.RtpPayloadFormat.getMimeTypeFromRtpMediaType;
import static com.google.android.exoplayer2.source.rtsp.sdp.MediaDescription.MEDIA_TYPE_AUDIO;
import static com.google.android.exoplayer2.source.rtsp.sdp.SessionDescription.ATTR_CONTROL;
import static com.google.android.exoplayer2.source.rtsp.sdp.SessionDescription.ATTR_RTPMAP;
import static com.google.android.exoplayer2.util.Assertions.checkArgument;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.android.exoplayer2.util.NalUnitUtil.NAL_START_CODE;
import static com.google.android.exoplayer2.util.Util.castNonNull;
import android.net.Uri;
import android.util.Base64;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.audio.AacUtil;
import com.google.android.exoplayer2.source.rtsp.rtp.RtpPayloadFormat;
import com.google.android.exoplayer2.source.rtsp.sdp.MediaDescription;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.NalUnitUtil;
import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
/** Represents a media track in an RTSP playback. */
public final class RtspMediaTrack {
// Format specific parameter names.
private static final String PARAMETER_PROFILE_LEVEL_ID = "profile-level-id";
private static final String PARAMETER_SPROP_PARAMS = "sprop-parameter-sets";
/** Prefix for the RFC6381 codecs string for AAC formats. */
private static final String AAC_CODECS_PREFIX = "mp4a.40.";
/** Prefix for the RFC6381 codecs string for AVC formats. */
private static final String H264_CODECS_PREFIX = "avc1.";
/** The track's associated {@link RtpPayloadFormat}. */
public final RtpPayloadFormat payloadFormat;
/** The track's URI. */
public final Uri uri;
/**
* Creates a new instance from a {@link MediaDescription}.
*
* @param mediaDescription The {@link MediaDescription} of this track.
* @param sessionUri The {@link Uri} of the RTSP playback session.
*/
public RtspMediaTrack(MediaDescription mediaDescription, Uri sessionUri) {
checkArgument(mediaDescription.attributes.containsKey(ATTR_CONTROL));
payloadFormat = generatePayloadFormat(mediaDescription);
uri =
sessionUri
.buildUpon()
.appendEncodedPath(castNonNull(mediaDescription.attributes.get(ATTR_CONTROL)))
.build();
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
RtspMediaTrack that = (RtspMediaTrack) o;
return payloadFormat.equals(that.payloadFormat) && uri.equals(that.uri);
}
@Override
public int hashCode() {
int result = 7;
result = 31 * result + payloadFormat.hashCode();
result = 31 * result + uri.hashCode();
return result;
}
@VisibleForTesting
/* package */ static RtpPayloadFormat generatePayloadFormat(MediaDescription mediaDescription) {
Format.Builder formatBuilder = new Format.Builder();
if (mediaDescription.bitrate > 0) {
formatBuilder.setAverageBitrate(mediaDescription.bitrate);
}
// rtpmap is mandatory in an RTSP session with dynamic payload types (RFC2326 Section C.1.3).
checkArgument(mediaDescription.attributes.containsKey(ATTR_RTPMAP));
String rtpmapAttribute = castNonNull(mediaDescription.attributes.get(ATTR_RTPMAP));
// rtpmap string format: RFC2327 Page 22.
String[] rtpmap = Util.split(rtpmapAttribute, " ");
checkArgument(rtpmap.length == 2);
int rtpPayloadType = mediaDescription.rtpMapAttribute.payloadType;
String mimeType = getMimeTypeFromRtpMediaType(mediaDescription.rtpMapAttribute.mediaEncoding);
formatBuilder.setSampleMimeType(mimeType);
int clockRate = mediaDescription.rtpMapAttribute.clockRate;
int channelCount = C.INDEX_UNSET;
if (MEDIA_TYPE_AUDIO.equals(mediaDescription.mediaType)) {
channelCount =
inferChannelCount(mediaDescription.rtpMapAttribute.encodingParameters, mimeType);
formatBuilder.setSampleRate(clockRate).setChannelCount(channelCount);
}
ImmutableMap<String, String> fmtpParameters = mediaDescription.getFmtpParametersAsMap();
switch (mimeType) {
case MimeTypes.AUDIO_AAC:
checkArgument(channelCount != C.INDEX_UNSET);
checkArgument(!fmtpParameters.isEmpty());
processAacFmtpAttribute(formatBuilder, fmtpParameters, channelCount, clockRate);
break;
case MimeTypes.VIDEO_H264:
checkArgument(!fmtpParameters.isEmpty());
processH264FmtpAttribute(formatBuilder, fmtpParameters);
break;
case MimeTypes.AUDIO_AC3:
// AC3 does not require a FMTP attribute. Fall through.
default:
// Do nothing.
}
checkArgument(clockRate > 0);
// Checks if payload type is "dynamic" as defined in RFC3551 Section 3.
checkArgument(rtpPayloadType >= 96);
return new RtpPayloadFormat(formatBuilder.build(), rtpPayloadType, clockRate, fmtpParameters);
}
private static int inferChannelCount(int encodingParameter, String mimeType) {
if (encodingParameter != C.INDEX_UNSET) {
// The encoding parameter specifies the number of channels in audio streams when
// present. If omitted, the number of channels is one. This parameter has no significance in
// video streams. (RFC2327 Page 22).
return encodingParameter;
}
if (mimeType.equals(MimeTypes.AUDIO_AC3)) {
// If RTPMAP attribute does not include channel count for AC3, default to 6.
return 6;
}
return 1;
}
private static void processAacFmtpAttribute(
Format.Builder formatBuilder,
ImmutableMap<String, String> fmtpAttributes,
int channelCount,
int sampleRate) {
checkArgument(fmtpAttributes.containsKey(PARAMETER_PROFILE_LEVEL_ID));
String profileLevel = checkNotNull(fmtpAttributes.get(PARAMETER_PROFILE_LEVEL_ID));
formatBuilder.setCodecs(AAC_CODECS_PREFIX + profileLevel);
formatBuilder.setInitializationData(
ImmutableList.of(
// Clock rate equals to sample rate in RTP.
AacUtil.buildAacLcAudioSpecificConfig(sampleRate, channelCount)));
}
private static void processH264FmtpAttribute(
Format.Builder formatBuilder, ImmutableMap<String, String> fmtpAttributes) {
checkArgument(fmtpAttributes.containsKey(PARAMETER_PROFILE_LEVEL_ID));
String profileLevel = checkNotNull(fmtpAttributes.get(PARAMETER_PROFILE_LEVEL_ID));
formatBuilder.setCodecs(H264_CODECS_PREFIX + profileLevel);
checkArgument(fmtpAttributes.containsKey(PARAMETER_SPROP_PARAMS));
String spropParameterSets = checkNotNull(fmtpAttributes.get(PARAMETER_SPROP_PARAMS));
String[] parameterSets = Util.split(spropParameterSets, ",");
checkArgument(parameterSets.length == 2);
ImmutableList<byte[]> initializationData =
ImmutableList.of(
getH264InitializationDataFromParameterSet(parameterSets[0]),
getH264InitializationDataFromParameterSet(parameterSets[1]));
formatBuilder.setInitializationData(initializationData);
// Process SPS (Sequence Parameter Set).
byte[] spsNalDataWithStartCode = initializationData.get(0);
NalUnitUtil.SpsData spsData =
NalUnitUtil.parseSpsNalUnit(
spsNalDataWithStartCode, NAL_START_CODE.length, spsNalDataWithStartCode.length);
formatBuilder.setPixelWidthHeightRatio(spsData.pixelWidthAspectRatio);
formatBuilder.setHeight(spsData.height);
formatBuilder.setWidth(spsData.width);
}
private static byte[] getH264InitializationDataFromParameterSet(String parameterSet) {
byte[] decodedParameterNalData = Base64.decode(parameterSet, Base64.DEFAULT);
byte[] decodedParameterNalUnit =
new byte[decodedParameterNalData.length + NAL_START_CODE.length];
System.arraycopy(
NAL_START_CODE,
/* srcPos= */ 0,
decodedParameterNalUnit,
/* destPos= */ 0,
NAL_START_CODE.length);
System.arraycopy(
decodedParameterNalData,
/* srcPos= */ 0,
decodedParameterNalUnit,
/* destPos= */ NAL_START_CODE.length,
decodedParameterNalData.length);
return decodedParameterNalUnit;
}
}

View File

@ -0,0 +1,105 @@
/*
* 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 com.google.android.exoplayer2.source.rtsp;
import static com.google.android.exoplayer2.util.Assertions.checkArgument;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.util.Util;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Represent the timing (RTSP Normal Playback Time format) of an RTSP session.
*
* <p>Currently only NPT is supported. See RFC2326 Section 3.6 for detail of NPT.
*/
public final class RtspSessionTiming {
/** The default session timing starting from 0.000 and indefinite length. */
public static final RtspSessionTiming DEFAULT =
new RtspSessionTiming(/* startTimeMs= */ 0, /* stopTimeMs= */ C.TIME_UNSET);
// We only support npt=xxx-[xxx], but not npt=-xxx. See RFC2326 Section 3.6.
private static final Pattern NPT_RANGE_PATTERN =
Pattern.compile("npt=([.\\d]+|now)\\s?-\\s?([.\\d]+)?");
private static final String START_TIMING_NTP_FORMAT = "npt=%.3f-";
private static final long LIVE_START_TIME = 0;
/** Parses an SDP range attribute (RFC2326 Section 3.6). */
public static RtspSessionTiming parseTiming(String sdpRangeAttribute) throws ParserException {
long startTimeMs;
long stopTimeMs;
Matcher matcher = NPT_RANGE_PATTERN.matcher(sdpRangeAttribute);
checkArgument(matcher.matches());
String startTimeString = checkNotNull(matcher.group(1));
if (startTimeString.equals("now")) {
startTimeMs = LIVE_START_TIME;
} else {
startTimeMs = (long) (Float.parseFloat(startTimeString) * C.MILLIS_PER_SECOND);
}
@Nullable String stopTimeString = matcher.group(2);
if (stopTimeString != null) {
try {
stopTimeMs = (long) (Float.parseFloat(stopTimeString) * C.MILLIS_PER_SECOND);
} catch (NumberFormatException e) {
throw new ParserException(e);
}
checkArgument(stopTimeMs > startTimeMs);
} else {
stopTimeMs = C.TIME_UNSET;
}
return new RtspSessionTiming(startTimeMs, stopTimeMs);
}
/** Gets a Range RTSP header for an RTSP PLAY request. */
public static String getOffsetStartTimeTiming(long offsetStartTimeMs) {
double offsetStartTimeSec = (double) offsetStartTimeMs / C.MILLIS_PER_SECOND;
return Util.formatInvariant(START_TIMING_NTP_FORMAT, offsetStartTimeSec);
}
/**
* The start time of this session, in milliseconds. When playing a live session, the start time is
* always zero.
*/
public final long startTimeMs;
/**
* The stop time of the session, in milliseconds, or {@link C#TIME_UNSET} when the stop time is
* not set, for example when playing a live session.
*/
public final long stopTimeMs;
private RtspSessionTiming(long startTimeMs, long stopTimeMs) {
this.startTimeMs = startTimeMs;
this.stopTimeMs = stopTimeMs;
}
/** Tests whether the timing is live. */
public boolean isLive() {
return stopTimeMs == C.TIME_UNSET;
}
/** Gets the session duration in milliseconds. */
public long getDurationMs() {
return stopTimeMs - startTimeMs;
}
}

View File

@ -0,0 +1,114 @@
/*
* 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 com.google.android.exoplayer2.source.rtsp;
import android.net.Uri;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.source.rtsp.message.RtspHeaders;
import com.google.android.exoplayer2.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).
*
* <p>The fields {@link #rtpTimestamp} and {@link #sequenceNumber} will not both be {@code null}.
*/
public 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.
* @return A list of parsed {@link RtspTrackTiming}.
* @throws ParserException If parsing failed.
*/
public static ImmutableList<RtspTrackTiming> parseTrackTiming(String rtpInfoString)
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 = Uri.parse(attributeValue);
break;
case "seq":
sequenceNumber = Integer.parseInt(attributeValue);
break;
case "rtptime":
rtpTime = Long.parseLong(attributeValue);
break;
default:
throw new ParserException();
}
} catch (Exception e) {
throw new ParserException(attributePair, e);
}
}
if (uri == null
|| uri.getScheme() == null // Checks if the URI is a URL.
|| (sequenceNumber == C.INDEX_UNSET && rtpTime == C.TIME_UNSET)) {
throw new ParserException(perTrackTimingString);
}
listBuilder.add(new RtspTrackTiming(rtpTime, sequenceNumber, uri));
}
return listBuilder.build();
}
/** The timestamp of the next RTP packet, {@link C#TIME_UNSET} if not present. */
public final long rtpTimestamp;
/** The sequence number of the next RTP packet, {@link C#INDEX_UNSET} if not present. */
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;
}
}

View File

@ -0,0 +1,37 @@
/*
* 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 com.google.android.exoplayer2.source.rtsp.message;
import com.google.android.exoplayer2.source.rtsp.sdp.SessionDescription;
/** Represents an RTSP DESCRIBE response. */
public final class RtspDescribeResponse {
/** The response's status code. */
public final int status;
/** The {@link SessionDescription} (see RFC2327) in the DESCRIBE response. */
public final SessionDescription sessionDescription;
/**
* Creates a new instance.
*
* @param status The response's status code.
* @param sessionDescription The {@link SessionDescription} in the DESCRIBE response.
*/
public RtspDescribeResponse(int status, SessionDescription sessionDescription) {
this.status = status;
this.sessionDescription = sessionDescription;
}
}

View File

@ -0,0 +1,166 @@
/*
* 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 com.google.android.exoplayer2.source.rtsp.message;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.util.Util;
import com.google.common.base.Ascii;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.util.ArrayList;
import java.util.LinkedHashMap;
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).
*/
public 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 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";
/** Builds {@link RtspHeaders} instances. */
public static final class Builder {
private final List<String> namesAndValues;
/** Creates a new instance. */
public Builder() {
namesAndValues = new ArrayList<>();
}
/**
* 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) {
namesAndValues.add(headerName.trim());
namesAndValues.add(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 ImmutableList<String> namesAndValues;
/**
* Gets the headers as a map, where the keys are the header names and values are the header
* values.
*
* @return The headers as a map. The keys of the map have follows those that are used to build
* this {@link RtspHeaders} instance.
*/
public ImmutableMap<String, String> asMap() {
Map<String, String> headers = new LinkedHashMap<>();
for (int i = 0; i < namesAndValues.size(); i += 2) {
headers.put(namesAndValues.get(i), namesAndValues.get(i + 1));
}
return ImmutableMap.copyOf(headers);
}
/**
* Returns a header value mapped to the argument, {@code null} if the header name is not recorded.
*/
@Nullable
public String get(String headerName) {
for (int i = namesAndValues.size() - 2; i >= 0; i -= 2) {
if (Ascii.equalsIgnoreCase(headerName, namesAndValues.get(i))) {
return namesAndValues.get(i + 1);
}
}
return null;
}
private RtspHeaders(Builder builder) {
this.namesAndValues = ImmutableList.copyOf(builder.namesAndValues);
}
}

View File

@ -0,0 +1,291 @@
/*
* 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 com.google.android.exoplayer2.source.rtsp.message;
import static com.google.android.exoplayer2.util.Assertions.checkStateNotNull;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.util.Log;
import com.google.android.exoplayer2.upstream.Loader;
import com.google.android.exoplayer2.upstream.Loader.LoadErrorAction;
import com.google.android.exoplayer2.upstream.Loader.Loadable;
import com.google.android.exoplayer2.util.Util;
import com.google.common.base.Charsets;
import com.google.common.base.Joiner;
import java.io.BufferedReader;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** Sends and receives RTSP messages. */
public final class RtspMessageChannel implements Closeable {
private static final String TAG = "RtspMessageChannel";
private static final boolean LOG_RTSP_MESSAGES = false;
/** A listener for received RTSP messages and possible failures. */
public interface MessageListener {
/**
* Called when an RTSP message is received.
*
* @param message The non-empty list of received lines, with line terminators removed.
*/
void onRtspMessageReceived(List<String> message);
/**
* Called when failed to send an RTSP message.
*
* @param message The list of lines making up the RTSP message that is failed to send.
* @param e The thrown {@link Exception}.
*/
default void onSendingFailed(List<String> message, Exception e) {}
/**
* Called when failed to receive an RTSP message.
*
* @param e The thrown {@link Exception}.
*/
default void onReceivingFailed(Exception e) {}
}
/**
* The IANA-registered default port for RTSP. See <a
* href="https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml">here</a>
*/
public static final int DEFAULT_RTSP_PORT = 554;
/**
* The handler for all {@code messageListener} interactions. Backed by the thread on which this
* class is constructed.
*/
private final Handler messageListenerHandler;
private final MessageListener messageListener;
private final Loader receiverLoader;
private @MonotonicNonNull Sender sender;
private @MonotonicNonNull Socket socket;
private boolean closed;
/**
* Constructs a new instance.
*
* <p>The constructor must be called on a {@link Looper} thread. The thread is also where {@link
* MessageListener} events are sent. User must construct a socket for RTSP and call {@link
* #openSocket} to open the connection before being able to send and receive, and {@link #close}
* it when done.
*
* <p>Note: all method invocations must be made from the thread on which this class is created.
*
* @param messageListener The {@link MessageListener} to receive events.
*/
public RtspMessageChannel(MessageListener messageListener) {
this.messageListenerHandler = Util.createHandlerForCurrentLooper();
this.messageListener = messageListener;
this.receiverLoader = new Loader("ExoPlayer:RtspMessageChannel:ReceiverLoader");
}
/**
* Opens the message channel to send and receive RTSP messages.
*
* <p>Note: If an {@link IOException} is thrown, callers must still call {@link #close()} to
* ensure that any partial effects of the invocation are cleaned up.
*
* @param socket An accepted {@link Socket}.
*/
public void openSocket(Socket socket) throws IOException {
this.socket = socket;
sender = new Sender(socket.getOutputStream());
receiverLoader.startLoading(
new Receiver(socket.getInputStream()),
new LoaderCallbackImpl(),
/* defaultMinRetryCount= */ 0);
}
/**
* Closes the RTSP message channel.
*
* <p>The closed instance must not be re-opened again. The {@link MessageListener} will not
* receive further messages after closing.
*
* @throws IOException If an error occurs closing the message channel.
*/
@Override
public void close() throws IOException {
if (sender != null) {
sender.close();
}
receiverLoader.release();
if (socket != null) {
socket.close();
}
messageListenerHandler.removeCallbacksAndMessages(/* token= */ null);
closed = true;
}
/**
* Sends a serialized RTSP message.
*
* @param message The list of strings representing the serialized RTSP message.
*/
public void send(List<String> message) {
checkStateNotNull(sender);
sender.send(message);
}
private static void logMessage(List<String> rtspMessage) {
if (LOG_RTSP_MESSAGES) {
Log.d(TAG, Joiner.on('\n').join(rtspMessage));
}
}
private final class Sender implements Closeable {
private final OutputStream outputStream;
private final HandlerThread senderThread;
private final Handler senderThreadHandler;
/**
* Creates a new instance.
*
* @param outputStream The {@link OutputStream} of the opened RTSP {@link Socket}, to which the
* request is sent. The caller needs to close the {@link OutputStream}.
*/
public Sender(OutputStream outputStream) {
this.outputStream = outputStream;
this.senderThread = new HandlerThread("ExoPlayer:RtspMessageChannel:Sender");
this.senderThread.start();
this.senderThreadHandler = new Handler(this.senderThread.getLooper());
}
/**
* Sends out RTSP messages that are in the forms of lists of strings.
*
* <p>If {@link Exception} is thrown while sending, the message {@link
* MessageListener#onSendingFailed} is dispatched to the thread that created the {@link
* RtspMessageChannel}.
*
* @param message The must of strings representing the serialized RTSP message.
*/
public void send(List<String> message) {
logMessage(message);
byte[] data = RtspMessageUtil.convertMessageToByteArray(message);
senderThreadHandler.post(
() -> {
try {
outputStream.write(data);
} catch (Exception e) {
messageListenerHandler.post(
() -> {
if (!closed) {
messageListener.onSendingFailed(message, e);
}
});
}
});
}
@Override
public void close() {
senderThreadHandler.post(senderThread::quit);
try {
// Waits until all the messages posted to the sender thread are handled.
senderThread.join();
} catch (InterruptedException e) {
senderThread.interrupt();
}
}
}
/** A {@link Loadable} for receiving RTSP responses. */
private final class Receiver implements Loadable {
private final BufferedReader inputStreamReader;
private volatile boolean loadCanceled;
/**
* Creates a new instance.
*
* @param inputStream The {@link InputStream} of the opened RTSP {@link Socket}, from which the
* {@link RtspResponse RtspResponses} are received. The caller needs to close the {@link
* InputStream}.
*/
public Receiver(InputStream inputStream) {
inputStreamReader = new BufferedReader(new InputStreamReader(inputStream, Charsets.UTF_8));
}
@Override
public void cancelLoad() {
loadCanceled = true;
}
@Override
public void load() throws IOException {
List<String> messageLines = new ArrayList<>();
while (!loadCanceled) {
String line;
while (inputStreamReader.ready() && (line = inputStreamReader.readLine()) != null) {
messageLines.add(line);
}
if (!messageLines.isEmpty()) {
List<String> message = new ArrayList<>(messageLines);
logMessage(message);
messageListenerHandler.post(
() -> {
if (!closed) {
messageListener.onRtspMessageReceived(message);
}
});
// Resets for the next response.
messageLines.clear();
}
}
}
}
private final class LoaderCallbackImpl implements Loader.Callback<Receiver> {
@Override
public void onLoadCompleted(Receiver loadable, long elapsedRealtimeMs, long loadDurationMs) {}
@Override
public void onLoadCanceled(
Receiver loadable, long elapsedRealtimeMs, long loadDurationMs, boolean released) {}
@Override
public LoadErrorAction onLoadError(
Receiver loadable,
long elapsedRealtimeMs,
long loadDurationMs,
IOException error,
int errorCount) {
messageListener.onReceivingFailed(error);
return Loader.DONT_RETRY;
}
}
}

View File

@ -0,0 +1,360 @@
/*
* 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 com.google.android.exoplayer2.source.rtsp.message;
import static com.google.android.exoplayer2.source.rtsp.message.RtspRequest.METHOD_ANNOUNCE;
import static com.google.android.exoplayer2.source.rtsp.message.RtspRequest.METHOD_DESCRIBE;
import static com.google.android.exoplayer2.source.rtsp.message.RtspRequest.METHOD_GET_PARAMETER;
import static com.google.android.exoplayer2.source.rtsp.message.RtspRequest.METHOD_OPTIONS;
import static com.google.android.exoplayer2.source.rtsp.message.RtspRequest.METHOD_PAUSE;
import static com.google.android.exoplayer2.source.rtsp.message.RtspRequest.METHOD_PLAY;
import static com.google.android.exoplayer2.source.rtsp.message.RtspRequest.METHOD_PLAY_NOTIFY;
import static com.google.android.exoplayer2.source.rtsp.message.RtspRequest.METHOD_RECORD;
import static com.google.android.exoplayer2.source.rtsp.message.RtspRequest.METHOD_REDIRECT;
import static com.google.android.exoplayer2.source.rtsp.message.RtspRequest.METHOD_SETUP;
import static com.google.android.exoplayer2.source.rtsp.message.RtspRequest.METHOD_SET_PARAMETER;
import static com.google.android.exoplayer2.source.rtsp.message.RtspRequest.METHOD_TEARDOWN;
import static com.google.android.exoplayer2.source.rtsp.message.RtspRequest.METHOD_UNSET;
import static com.google.android.exoplayer2.util.Assertions.checkArgument;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import android.net.Uri;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.util.Util;
import com.google.common.base.Charsets;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** Utility methods for RTSP messages. */
public final class RtspMessageUtil {
/** Represents a RTSP Session header (RFC2326 Section 12.37). */
public static final class RtspSessionHeader {
/** The session ID. */
public final String sessionId;
/**
* The session timeout, measured in milliseconds, {@link #DEFAULT_RTSP_TIMEOUT_MS} if not
* specified in the Session header.
*/
public final long timeoutMs;
/** Creates a new instance. */
public RtspSessionHeader(String sessionId, long timeoutMs) {
this.sessionId = sessionId;
this.timeoutMs = timeoutMs;
}
}
/** The default timeout, in milliseconds, defined for RTSP (RFC2326 Section 12.37). */
public static final long DEFAULT_RTSP_TIMEOUT_MS = 60_000;
// Status line pattern, see RFC2326 Section 6.1.
private static final Pattern REQUEST_LINE_PATTERN = Pattern.compile("([A-Z_]+) (.*) RTSP/1\\.0");
// Status line pattern, see RFC2326 Section 7.1.
private static final Pattern STATUS_LINE_PATTERN = Pattern.compile("RTSP/1\\.0 (\\d+) (.+)");
// Session header pattern, see RFC2326 Section 12.37.
private static final Pattern SESSION_HEADER_PATTERN =
Pattern.compile("(\\w+)(?:;\\s?timeout=(\\d+))?");
private static final String RTSP_VERSION = "RTSP/1.0";
/**
* Serializes an {@link RtspRequest} to an {@link ImmutableList} of strings.
*
* @param request The {@link RtspRequest}.
* @return A list of the lines of the {@link RtspRequest}, without line terminators (CRLF).
*/
public static ImmutableList<String> serializeRequest(RtspRequest request) {
ImmutableList.Builder<String> builder = new ImmutableList.Builder<>();
// Request line.
builder.add(
Util.formatInvariant(
"%s %s %s", toMethodString(request.method), request.uri, RTSP_VERSION));
ImmutableMap<String, String> headers = request.headers.asMap();
for (String headerName : headers.keySet()) {
builder.add(
Util.formatInvariant(
"%s: %s", headerName, checkNotNull(request.headers.get(headerName))));
}
// Empty line after headers.
builder.add("");
builder.add(request.messageBody);
return builder.build();
}
/**
* Serializes an {@link RtspResponse} to an {@link ImmutableList} of strings.
*
* @param response The {@link RtspResponse}.
* @return A list of the lines of the {@link RtspResponse}, without line terminators (CRLF).
*/
public static ImmutableList<String> serializeResponse(RtspResponse response) {
ImmutableList.Builder<String> builder = new ImmutableList.Builder<>();
// Request line.
builder.add(
Util.formatInvariant(
"%s %s %s", RTSP_VERSION, response.status, getRtspStatusReasonPhrase(response.status)));
ImmutableMap<String, String> headers = response.headers.asMap();
for (String headerName : headers.keySet()) {
builder.add(
Util.formatInvariant(
"%s: %s", headerName, checkNotNull(response.headers.get(headerName))));
}
// Empty line after headers.
builder.add("");
builder.add(response.messageBody);
return builder.build();
}
/**
* Converts an RTSP message to a byte array.
*
* @param message The non-empty list of the lines of an RTSP message, with line terminators
* removed.
*/
public static byte[] convertMessageToByteArray(List<String> message) {
return Joiner.on("\r\n").join(message).getBytes(Charsets.UTF_8);
}
/** Removes the user info from the supplied {@link Uri}. */
public static Uri removeUserInfo(Uri uri) {
if (uri.getUserInfo() == null) {
return uri;
}
// The Uri must include a "@" if the user info is non-null.
String authorityWithUserInfo = checkNotNull(uri.getAuthority());
checkArgument(authorityWithUserInfo.contains("@"));
String authority = Util.split(authorityWithUserInfo, "@")[1];
return uri.buildUpon().encodedAuthority(authority).build();
}
/** Returns the corresponding String representation of the {@link RtspRequest.Method} argument. */
public static String toMethodString(@RtspRequest.Method int method) {
switch (method) {
case RtspRequest.METHOD_ANNOUNCE:
return "ANNOUNCE";
case METHOD_DESCRIBE:
return "DESCRIBE";
case METHOD_GET_PARAMETER:
return "GET_PARAMETER";
case METHOD_OPTIONS:
return "OPTIONS";
case METHOD_PAUSE:
return "PAUSE";
case METHOD_PLAY:
return "PLAY";
case METHOD_PLAY_NOTIFY:
return "PLAY_NOTIFY";
case METHOD_RECORD:
return "RECORD";
case METHOD_REDIRECT:
return "REDIRECT";
case METHOD_SETUP:
return "SETUP";
case METHOD_SET_PARAMETER:
return "SET_PARAMETER";
case METHOD_TEARDOWN:
return "TEARDOWN";
case METHOD_UNSET:
default:
throw new IllegalStateException();
}
}
@RtspRequest.Method
private static int parseMethodString(String method) {
switch (method) {
case "ANNOUNCE":
return METHOD_ANNOUNCE;
case "DESCRIBE":
return METHOD_DESCRIBE;
case "GET_PARAMETER":
return METHOD_GET_PARAMETER;
case "OPTIONS":
return METHOD_OPTIONS;
case "PAUSE":
return METHOD_PAUSE;
case "PLAY":
return METHOD_PLAY;
case "PLAY_NOTIFY":
return METHOD_PLAY_NOTIFY;
case "RECORD":
return METHOD_RECORD;
case "REDIRECT":
return METHOD_REDIRECT;
case "SETUP":
return METHOD_SETUP;
case "SET_PARAMETER":
return METHOD_SET_PARAMETER;
case "TEARDOWN":
return METHOD_TEARDOWN;
default:
throw new IllegalArgumentException();
}
}
/**
* Parses lines of a received RTSP response into an {@link RtspResponse} instance.
*
* @param lines The non-empty list of received lines, with line terminators removed.
* @return The parsed {@link RtspResponse} object.
*/
public static RtspResponse parseResponse(List<String> lines) {
Matcher statusLineMatcher = STATUS_LINE_PATTERN.matcher(lines.get(0));
checkArgument(statusLineMatcher.matches());
int statusCode = Integer.parseInt(checkNotNull(statusLineMatcher.group(1)));
// An empty line marks the boundary between header and body.
int messageBodyOffset = lines.indexOf("");
checkArgument(messageBodyOffset > 0);
List<String> headerLines = lines.subList(1, messageBodyOffset);
RtspHeaders headers = new RtspHeaders.Builder().addAll(headerLines).build();
String messageBody = Joiner.on("\r\n").join(lines.subList(messageBodyOffset + 1, lines.size()));
return new RtspResponse(statusCode, headers, messageBody);
}
/**
* Parses lines of a received RTSP request into an {@link RtspRequest} instance.
*
* @param lines The non-empty list of received lines, with line terminators removed.
* @return The parsed {@link RtspRequest} object.
*/
public static RtspRequest parseRequest(List<String> lines) {
Matcher requestMatcher = REQUEST_LINE_PATTERN.matcher(lines.get(0));
checkArgument(requestMatcher.matches());
@RtspRequest.Method int method = parseMethodString(checkNotNull(requestMatcher.group(1)));
Uri requestUri = Uri.parse(checkNotNull(requestMatcher.group(2)));
// An empty line marks the boundary between header and body.
int messageBodyOffset = lines.indexOf("");
checkArgument(messageBodyOffset > 0);
List<String> headerLines = lines.subList(1, messageBodyOffset);
RtspHeaders headers = new RtspHeaders.Builder().addAll(headerLines).build();
String messageBody = Joiner.on("\r\n").join(lines.subList(messageBodyOffset + 1, lines.size()));
return new RtspRequest(requestUri, method, headers, messageBody);
}
/**
* Parses the RTSP PUBLIC header into a list of RTSP methods.
*
* @param publicHeader The PUBLIC header content, null if not available.
* @return The list of supported RTSP methods, encoded in {@link RtspRequest.Method}, or an empty
* list if the PUBLIC header is null.
*/
public static ImmutableList<Integer> parsePublicHeader(@Nullable String publicHeader) {
if (publicHeader == null) {
return ImmutableList.of();
}
ImmutableList.Builder<Integer> methodListBuilder = new ImmutableList.Builder<>();
for (String method : Util.split(publicHeader, ",\\s?")) {
methodListBuilder.add(parseMethodString(method));
}
return methodListBuilder.build();
}
/**
* Parses a Session header in an RTSP message to {@link RtspSessionHeader}.
*
* <p>The format of the Session header is
*
* <pre>
* Session: session-id[;timeout=delta-seconds]
* </pre>
*
* @param headerValue The string represent the content without the header name (Session: ).
* @return The parsed {@link RtspSessionHeader}.
* @throws ParserException When the input header value does not follow the Session header format.
*/
public static RtspSessionHeader parseSessionHeader(String headerValue) throws ParserException {
Matcher matcher = SESSION_HEADER_PATTERN.matcher(headerValue);
if (!matcher.matches()) {
throw new ParserException(headerValue);
}
String sessionId = checkNotNull(matcher.group(1));
// Optional parameter timeout.
long timeoutMs = DEFAULT_RTSP_TIMEOUT_MS;
@Nullable String timeoutString;
if ((timeoutString = matcher.group(2)) != null) {
try {
timeoutMs = Integer.parseInt(timeoutString) * C.MILLIS_PER_SECOND;
} catch (NumberFormatException e) {
throw new ParserException(e);
}
}
return new RtspSessionHeader(sessionId, timeoutMs);
}
private static String getRtspStatusReasonPhrase(int statusCode) {
switch (statusCode) {
case 200:
return "OK";
case 400:
return "Bad Request";
case 401:
return "Unauthorized";
case 404:
return "Not Found";
case 405:
return "Method Not Allowed";
case 454:
return "Session Not Found";
case 455:
return "Method Not Valid In This State";
case 456:
return "Header Field Not Valid";
case 457:
return "Invalid Range";
case 461:
return "Unsupported Transport";
case 500:
return "Internal Server Error";
case 505:
return "RTSP Version Not Supported";
default:
throw new IllegalArgumentException();
}
}
/**
* Parses the string argument as an integer, wraps the potential {@link NumberFormatException} in
* {@link ParserException}.
*/
public static int parseInt(String intString) throws ParserException {
try {
return Integer.parseInt(intString);
} catch (NumberFormatException e) {
throw new ParserException(e);
}
}
private RtspMessageUtil() {}
}

View File

@ -0,0 +1,44 @@
/*
* 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 com.google.android.exoplayer2.source.rtsp.message;
import com.google.common.collect.ImmutableList;
import java.util.List;
/** Represents an RTSP OPTIONS response. */
// TODO(b/180434754) Move all classes under message to the parent rtsp package, and change the
// visibility.
public final class RtspOptionsResponse {
/** The response's status code. */
public final int status;
/**
* A list of methods supported by the RTSP server, encoded as {@link RtspRequest.Method}; or an
* empty list if the server does not disclose the supported methods.
*/
public final ImmutableList<Integer> supportedMethods;
/**
* Creates a new instance.
*
* @param status The response's status code.
* @param supportedMethods A list of methods supported by the RTSP server, encoded as {@link
* RtspRequest.Method}; or an empty list if such information is not available.
*/
public RtspOptionsResponse(int status, List<Integer> supportedMethods) {
this.status = status;
this.supportedMethods = ImmutableList.copyOf(supportedMethods);
}
}

View File

@ -0,0 +1,47 @@
/*
* 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 com.google.android.exoplayer2.source.rtsp.message;
import com.google.android.exoplayer2.source.rtsp.RtspSessionTiming;
import com.google.android.exoplayer2.source.rtsp.RtspTrackTiming;
import com.google.common.collect.ImmutableList;
import java.util.List;
/** Represents an RTSP PLAY response. */
public final class RtspPlayResponse {
/** The response's status code. */
public final int status;
/** The playback start timing, {@link RtspSessionTiming#DEFAULT} if not present. */
public final RtspSessionTiming sessionTiming;
/** The list of {@link RtspTrackTiming} representing the {@link RtspHeaders#RTP_INFO} header. */
public final ImmutableList<RtspTrackTiming> trackTimingList;
/**
* Creates a new instance.
*
* @param status The response's status code.
* @param sessionTiming The {@link RtspSessionTiming}, pass {@link RtspSessionTiming#DEFAULT} if
* not present.
* @param trackTimingList The list of {@link RtspTrackTiming} representing the {@link
* RtspHeaders#RTP_INFO} header.
*/
public RtspPlayResponse(
int status, RtspSessionTiming sessionTiming, List<RtspTrackTiming> trackTimingList) {
this.status = status;
this.sessionTiming = sessionTiming;
this.trackTimingList = ImmutableList.copyOf(trackTimingList);
}
}

View File

@ -0,0 +1,105 @@
/*
* 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 com.google.android.exoplayer2.source.rtsp.message;
import android.net.Uri;
import androidx.annotation.IntDef;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/** Represents an RTSP request. */
public final class RtspRequest {
/**
* RTSP request methods, as defined in RFC2326 Section 10.
*
* <p>The possible values are:
*
* <ul>
* <li>{@link #METHOD_UNSET}
* <li>{@link #METHOD_ANNOUNCE}
* <li>{@link #METHOD_DESCRIBE}
* <li>{@link #METHOD_GET_PARAMETER}
* <li>{@link #METHOD_OPTIONS}
* <li>{@link #METHOD_PAUSE}
* <li>{@link #METHOD_PLAY}
* <li>{@link #METHOD_PLAY_NOTIFY}
* <li>{@link #METHOD_RECORD}
* <li>{@link #METHOD_REDIRECT}
* <li>{@link #METHOD_SETUP}
* <li>{@link #METHOD_SET_PARAMETER}
* <li>{@link #METHOD_TEARDOWN}
* </ul>
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef(
value = {
METHOD_UNSET,
METHOD_ANNOUNCE,
METHOD_DESCRIBE,
METHOD_GET_PARAMETER,
METHOD_OPTIONS,
METHOD_PAUSE,
METHOD_PLAY,
METHOD_PLAY_NOTIFY,
METHOD_RECORD,
METHOD_REDIRECT,
METHOD_SETUP,
METHOD_SET_PARAMETER,
METHOD_TEARDOWN
})
public @interface Method {}
public static final int METHOD_UNSET = 0;
public static final int METHOD_ANNOUNCE = 1;
public static final int METHOD_DESCRIBE = 2;
public static final int METHOD_GET_PARAMETER = 3;
public static final int METHOD_OPTIONS = 4;
public static final int METHOD_PAUSE = 5;
public static final int METHOD_PLAY = 6;
public static final int METHOD_PLAY_NOTIFY = 7;
public static final int METHOD_RECORD = 8;
public static final int METHOD_REDIRECT = 9;
public static final int METHOD_SETUP = 10;
public static final int METHOD_SET_PARAMETER = 11;
public static final int METHOD_TEARDOWN = 12;
/** The {@link Uri} to which this request is sent. */
public final Uri uri;
/** The request method, as defined in {@link Method}. */
@Method public final int method;
/** The headers of this request. */
public final RtspHeaders headers;
/** The body of this RTSP message, or empty string if absent. */
public final String messageBody;
/**
* Creates a new instance.
*
* @param uri The {@link Uri} to which this request is sent.
* @param method The request method, as defined in {@link Method}.
* @param headers The headers of this request.
* @param messageBody The body of this RTSP message, or empty string if absent.
*/
public RtspRequest(Uri uri, @Method int method, RtspHeaders headers, String messageBody) {
this.uri = uri;
this.method = method;
this.headers = headers;
this.messageBody = messageBody;
}
}

View File

@ -0,0 +1,44 @@
/*
* 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 com.google.android.exoplayer2.source.rtsp.message;
/** Represents an RTSP Response. */
public final class RtspResponse {
// TODO(b/172331505) Move this constant to MimeTypes.
/** The MIME type associated with Session Description Protocol (RFC4566). */
public static final String SDP_MIME_TYPE = "application/sdp";
/** The status code of this response, as defined in RFC 2326 section 11. */
public final int status;
/** The headers of this response. */
public final RtspHeaders headers;
/** The body of this RTSP message, or empty string if absent. */
public final String messageBody;
/**
* Creates a new instance.
*
* @param status The status code of this response, as defined in RFC 2326 section 11.
* @param headers The headers of this response.
* @param messageBody The body of this RTSP message, or empty string if absent.
*/
public RtspResponse(int status, RtspHeaders headers, String messageBody) {
this.status = status;
this.headers = headers;
this.messageBody = messageBody;
}
}

View File

@ -0,0 +1,43 @@
/*
* 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 com.google.android.exoplayer2.source.rtsp.message;
/** Represents an RTSP SETUP response. */
// TODO(b/180434754) Move all classes under message to the parent rtsp package, and change the
// visibility.
public final class RtspSetupResponse {
/** The response's status code. */
public final int status;
/** The Session header (RFC2326 Section 12.37). */
public final RtspMessageUtil.RtspSessionHeader sessionHeader;
/** The Transport header (RFC2326 Section 12.39). */
public final String transport;
/**
* Creates a new instance.
*
* @param status The response's status code.
* @param sessionHeader The {@link RtspMessageUtil.RtspSessionHeader}.
* @param transport The transport header included in the RTSP SETUP response.
*/
public RtspSetupResponse(
int status, RtspMessageUtil.RtspSessionHeader sessionHeader, String transport) {
this.status = status;
this.sessionHeader = sessionHeader;
this.transport = transport;
}
}

View File

@ -0,0 +1,19 @@
/*
* Copyright 2020 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.
*/
@NonNullApi
package com.google.android.exoplayer2.source.rtsp.message;
import com.google.android.exoplayer2.util.NonNullApi;

View File

@ -0,0 +1,19 @@
/*
* Copyright 2020 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.
*/
@NonNullApi
package com.google.android.exoplayer2.source.rtsp;
import com.google.android.exoplayer2.util.NonNullApi;

View File

@ -0,0 +1,211 @@
/*
* Copyright 2020 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 com.google.android.exoplayer2.source.rtsp.rtp;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.android.exoplayer2.util.Util.closeQuietly;
import android.net.Uri;
import android.os.Handler;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.DefaultExtractorInput;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.source.rtsp.RtspMediaTrack;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.Loader;
import com.google.android.exoplayer2.upstream.UdpDataSource;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* A {@link Loader.Loadable} that sets up a sockets listening to incoming RTP traffic, carried by
* UDP packets.
*
* <p>Uses a {@link UdpDataSource} to listen on incoming packets. The local UDP port is selected by
* the runtime on opening; it also opens another {@link UdpDataSource} for RTCP on the RTP UDP port
* number plus one one. Pass a listener via constructor to receive a callback when the local port is
* opened. {@link #load} will throw an {@link IOException} if either of the two data sources fails
* to open.
*
* <p>Received RTP packets' payloads will be extracted by an {@link RtpExtractor}, and will be
* written to the {@link ExtractorOutput} instance provided at construction.
*/
public final class RtpDataLoadable implements Loader.Loadable {
/** Called on loadable events. */
public interface EventListener {
/**
* Called when the transport information for receiving incoming RTP and RTCP packets is ready.
*
* @param transport The RTSP transport (RFC2326 Section 12.39) including the client data port
* and RTCP port.
*/
void onTransportReady(String transport);
}
private static final String DEFAULT_TRANSPORT_FORMAT = "RTP/AVP;unicast;client_port=%d-%d";
private static final String RTP_ANY_INCOMING_IPV4 = "rtp://0.0.0.0";
// Using port zero will cause the system to generate a port.
private static final int RTP_LOCAL_PORT = 0;
private static final String RTP_BIND_ADDRESS = RTP_ANY_INCOMING_IPV4 + ":" + RTP_LOCAL_PORT;
/** The track ID associated with the Loadable. */
public final int trackId;
/** The {@link RtspMediaTrack} to load. */
public final RtspMediaTrack rtspMediaTrack;
private final EventListener eventListener;
private final ExtractorOutput output;
private final Handler playbackThreadHandler;
private @MonotonicNonNull RtpExtractor extractor;
private volatile boolean loadCancelled;
private volatile long pendingSeekPositionUs;
private volatile long nextRtpTimestamp;
/**
* Creates an {@link RtpDataLoadable} that listens on incoming RTP traffic.
*
* <p>Caller of this constructor must be on playback thread.
*
* @param trackId The track ID associated with the Loadable.
* @param rtspMediaTrack The {@link RtspMediaTrack} to load.
* @param eventListener The {@link EventListener}.
* @param output A {@link ExtractorOutput} instance to which the received and extracted data will
*/
public RtpDataLoadable(
int trackId,
RtspMediaTrack rtspMediaTrack,
EventListener eventListener,
ExtractorOutput output) {
this.trackId = trackId;
this.rtspMediaTrack = rtspMediaTrack;
this.eventListener = eventListener;
this.output = output;
this.playbackThreadHandler = Util.createHandlerForCurrentLooper();
pendingSeekPositionUs = C.TIME_UNSET;
}
/**
* Sets the timestamp of an RTP packet to arrive.
*
* @param timestamp The timestamp of the RTP packet to arrive. Supply {@link C#TIME_UNSET} if its
* unavailable.
*/
public void setTimestamp(long timestamp) {
if (timestamp != C.TIME_UNSET) {
if (!checkNotNull(extractor).hasReadFirstRtpPacket()) {
extractor.setFirstTimestamp(timestamp);
}
}
}
/**
* Sets the timestamp of an RTP packet to arrive.
*
* @param sequenceNumber The sequence number of the RTP packet to arrive. Supply {@link
* C#INDEX_UNSET} if its unavailable.
*/
public void setSequenceNumber(int sequenceNumber) {
if (!checkNotNull(extractor).hasReadFirstRtpPacket()) {
extractor.setFirstSequenceNumber(sequenceNumber);
}
}
@Override
public void cancelLoad() {
loadCancelled = true;
}
@Override
public void load() throws IOException {
@Nullable UdpDataSource firstDataSource = null;
@Nullable UdpDataSource secondDataSource = null;
try {
// Open and set up the data sources.
// From RFC3550 Section 11: "For UDP and similar protocols, RTP SHOULD use an even destination
// port number and the corresponding RTCP stream SHOULD use the next higher (odd) destination
// port number". Some RTSP servers are strict about this rule.
// We open a data source first, and depending its port number, open the next data source with
// a port number that is either the higher or the lower.
firstDataSource = new UdpDataSource();
firstDataSource.open(new DataSpec(Uri.parse(RTP_BIND_ADDRESS)));
int firstPort = firstDataSource.getLocalPort();
boolean isFirstPortNumberEven = (firstPort % 2 == 0);
int secondPort = isFirstPortNumberEven ? firstPort + 1 : firstPort - 1;
// RTCP always uses the immediate next port.
secondDataSource = new UdpDataSource();
secondDataSource.open(new DataSpec(Uri.parse(RTP_ANY_INCOMING_IPV4 + ":" + secondPort)));
// RTP data port is always the lower and even-numbered port.
UdpDataSource dataSource = isFirstPortNumberEven ? firstDataSource : secondDataSource;
int dataPort = dataSource.getLocalPort();
int rtcpPort = dataPort + 1;
String transport = Util.formatInvariant(DEFAULT_TRANSPORT_FORMAT, dataPort, rtcpPort);
playbackThreadHandler.post(() -> eventListener.onTransportReady(transport));
// Sets up the extractor.
ExtractorInput extractorInput =
new DefaultExtractorInput(
checkNotNull(dataSource), /* position= */ 0, /* length= */ C.LENGTH_UNSET);
extractor = new RtpExtractor(rtspMediaTrack.payloadFormat, trackId);
extractor.init(output);
while (!loadCancelled) {
if (pendingSeekPositionUs != C.TIME_UNSET) {
extractor.seek(nextRtpTimestamp, pendingSeekPositionUs);
pendingSeekPositionUs = C.TIME_UNSET;
}
extractor.read(extractorInput, /* seekPosition= */ new PositionHolder());
}
} finally {
closeQuietly(firstDataSource);
closeQuietly(secondDataSource);
}
}
/**
* Signals when performing an RTSP seek that involves RTSP message exchange.
*
* <p>{@link #seekToUs} must be called after the seek is successful.
*/
public void resetForSeek() {
checkNotNull(extractor).preSeek();
}
/**
* Sets the correct start position and RTP timestamp after a successful RTSP seek.
*
* @param positionUs The position in microseconds from the start, from which the server starts
* play.
* @param nextRtpTimestamp The first RTP packet's timestamp after the seek.
*/
public void seekToUs(long positionUs, long nextRtpTimestamp) {
pendingSeekPositionUs = positionUs;
this.nextRtpTimestamp = nextRtpTimestamp;
}
}

View File

@ -0,0 +1,205 @@
/*
* Copyright 2020 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 com.google.android.exoplayer2.source.rtsp.rtp;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import android.os.SystemClock;
import androidx.annotation.GuardedBy;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.source.rtsp.rtp.reader.DefaultRtpPayloadReaderFactory;
import com.google.android.exoplayer2.source.rtsp.rtp.reader.RtpPayloadReader;
import com.google.android.exoplayer2.util.ParsableByteArray;
import java.io.IOException;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** Extracts data from RTP packets. */
/* package */ final class RtpExtractor implements Extractor {
private final RtpPayloadReader payloadReader;
private final ParsableByteArray rtpPacketScratchBuffer;
private final ParsableByteArray rtpPacketDataBuffer;
private final int trackId;
private final Object lock;
private final RtpPacketReorderingQueue reorderingQueue;
private @MonotonicNonNull ExtractorOutput output;
private boolean firstPacketRead;
private volatile long firstTimestamp;
private volatile int firstSequenceNumber;
@GuardedBy("lock")
private boolean isSeekPending;
@GuardedBy("lock")
private long nextRtpTimestamp;
@GuardedBy("lock")
private long playbackStartTimeUs;
public RtpExtractor(RtpPayloadFormat payloadFormat, int trackId) {
this.trackId = trackId;
payloadReader =
checkNotNull(new DefaultRtpPayloadReaderFactory().createPayloadReader(payloadFormat));
rtpPacketScratchBuffer = new ParsableByteArray(RtpPacket.MAX_SIZE);
rtpPacketDataBuffer = new ParsableByteArray();
lock = new Object();
reorderingQueue = new RtpPacketReorderingQueue();
firstTimestamp = C.TIME_UNSET;
firstSequenceNumber = C.INDEX_UNSET;
nextRtpTimestamp = C.TIME_UNSET;
playbackStartTimeUs = C.TIME_UNSET;
}
/** Sets the timestamp of the first RTP packet to arrive. */
public void setFirstTimestamp(long firstTimestamp) {
this.firstTimestamp = firstTimestamp;
}
/** Sets the sequence number of the first RTP packet to arrive. */
public void setFirstSequenceNumber(int firstSequenceNumber) {
this.firstSequenceNumber = firstSequenceNumber;
}
/** Returns whether the first RTP packet is processed. */
public boolean hasReadFirstRtpPacket() {
return firstPacketRead;
}
/**
* Signals when performing an RTSP seek that involves RTSP message exchange.
*
* <p>{@link #seek} must be called after a successful RTSP seek.
*
* <p>After this method in called, the incoming RTP packets are read from the {@link
* ExtractorInput}, but they are not further processed by the {@link RtpPayloadReader readers}.
*
* <p>The user must clear the {@link ExtractorOutput} after calling this method, to ensure no
* samples are written to {@link ExtractorOutput}.
*/
public void preSeek() {
synchronized (lock) {
isSeekPending = true;
}
}
@Override
public boolean sniff(ExtractorInput input) {
// TODO(b/172331505) Build sniff support.
return false;
}
@Override
public void init(ExtractorOutput output) {
payloadReader.createTracks(output, trackId);
output.endTracks();
// TODO(b/172331505) replace hardcoded unseekable seekmap.
output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET));
this.output = output;
}
@Override
public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException {
checkNotNull(output); // Asserts init is called.
// Reads one RTP packet at a time.
int bytesRead = input.read(rtpPacketScratchBuffer.getData(), 0, RtpPacket.MAX_SIZE);
if (bytesRead == RESULT_END_OF_INPUT) {
return RESULT_END_OF_INPUT;
} else if (bytesRead == 0) {
return RESULT_CONTINUE;
}
rtpPacketScratchBuffer.setPosition(0);
rtpPacketScratchBuffer.setLimit(bytesRead);
@Nullable RtpPacket packet = RtpPacket.parse(rtpPacketScratchBuffer);
if (packet == null) {
return RESULT_CONTINUE;
}
long packetArrivalTimeMs = SystemClock.elapsedRealtime();
reorderingQueue.offer(packet, packetArrivalTimeMs);
@Nullable RtpPacket dequeuedPacket = reorderingQueue.poll(getCutoffTimeMs(packetArrivalTimeMs));
if (dequeuedPacket == null) {
// No packet is available for reading.
return RESULT_CONTINUE;
}
packet = dequeuedPacket;
if (!firstPacketRead) {
// firstTimestamp and firstSequenceNumber are transmitted over RTSP. There is no guarantee
// that they arrive before the RTP packets. We use whichever comes first.
if (firstTimestamp == C.TIME_UNSET) {
firstTimestamp = packet.timestamp;
}
if (firstSequenceNumber == C.INDEX_UNSET) {
firstSequenceNumber = packet.sequenceNumber;
}
payloadReader.onReceivingFirstPacket(firstTimestamp, firstSequenceNumber);
firstPacketRead = true;
}
synchronized (lock) {
// Ignores the incoming packets while seek is pending.
if (isSeekPending) {
if (nextRtpTimestamp != C.TIME_UNSET && playbackStartTimeUs != C.TIME_UNSET) {
payloadReader.seek(nextRtpTimestamp, playbackStartTimeUs);
isSeekPending = false;
nextRtpTimestamp = C.TIME_UNSET;
playbackStartTimeUs = C.TIME_UNSET;
}
} else {
rtpPacketDataBuffer.reset(packet.payloadData);
payloadReader.consume(
rtpPacketDataBuffer, packet.timestamp, packet.sequenceNumber, packet.marker);
}
}
return RESULT_CONTINUE;
}
@Override
public void seek(long nextRtpTimestamp, long playbackStartTimeUs) {
synchronized (lock) {
this.nextRtpTimestamp = nextRtpTimestamp;
this.playbackStartTimeUs = playbackStartTimeUs;
}
}
@Override
public void release() {
// Do nothing.
}
/**
* Returns the cutoff time of waiting for an out-of-order packet.
*
* <p>Returns the cutoff time to pass to {@link RtpPacketReorderingQueue#poll(long)} based on the
* given RtpPacket arrival time.
*/
private static long getCutoffTimeMs(long packetArrivalTimeMs) {
// TODO(internal b/172331505) 30ms is roughly the time for one video frame. It is not rigorously
// chosen and will need fine tuning in the future.
return packetArrivalTimeMs - 30;
}
}

View File

@ -0,0 +1,324 @@
/*
* Copyright 2020 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 com.google.android.exoplayer2.source.rtsp.rtp;
import static com.google.android.exoplayer2.util.Assertions.checkArgument;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
import java.nio.ByteBuffer;
/**
* Represents the header and the payload of an RTP packet.
*
* <p>Not supported parsing at the moment: header extension and CSRC.
*
* <p>Structure of an RTP header (RFC3550, Section 5.1).
*
* <pre>
* 0 1 2 3
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* |V=2|P|X| CC |M| PT | sequence number |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | timestamp |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | synchronization source (SSRC) identifier |
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
* | contributing source (CSRC) identifiers |
* | .... |
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
* | Profile-specific extension ID | Extension header length |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | Extension header |
* | .... |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* 3 2 1
* 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0
* </pre>
*/
public final class RtpPacket {
/** Builder class for an {@link RtpPacket} */
public static final class Builder {
private boolean padding;
private boolean marker;
private byte payloadType;
private int sequenceNumber;
private long timestamp;
private int ssrc;
private byte[] csrc = EMPTY;
private byte[] payloadData = EMPTY;
/** Sets the {@link RtpPacket#padding}. The default is false. */
public Builder setPadding(boolean padding) {
this.padding = padding;
return this;
}
/** Sets {@link RtpPacket#marker}. The default is false. */
public Builder setMarker(boolean marker) {
this.marker = marker;
return this;
}
/** Sets {@link RtpPacket#payloadType}. The default is 0. */
public Builder setPayloadType(byte payloadType) {
this.payloadType = payloadType;
return this;
}
/** Sets {@link RtpPacket#sequenceNumber}. The default is 0. */
public Builder setSequenceNumber(int sequenceNumber) {
checkArgument(sequenceNumber >= MIN_SEQUENCE_NUMBER && sequenceNumber <= MAX_SEQUENCE_NUMBER);
this.sequenceNumber = sequenceNumber & 0xFFFF;
return this;
}
/** Sets {@link RtpPacket#timestamp}. The default is 0. */
public Builder setTimestamp(long timestamp) {
this.timestamp = timestamp;
return this;
}
/** Sets {@link RtpPacket#ssrc}. The default is 0. */
public Builder setSsrc(int ssrc) {
this.ssrc = ssrc;
return this;
}
/** Sets {@link RtpPacket#csrc}. The default is an empty byte array. */
public Builder setCsrc(byte[] csrc) {
checkNotNull(csrc);
this.csrc = csrc;
return this;
}
/** Sets {@link RtpPacket#payloadData}. The default is an empty byte array. */
public Builder setPayloadData(byte[] payloadData) {
checkNotNull(payloadData);
this.payloadData = payloadData;
return this;
}
/** Builds the {@link RtpPacket}. */
public RtpPacket build() {
return new RtpPacket(this);
}
}
public static final int RTP_VERSION = 2;
public static final int MAX_SIZE = 65507;
public static final int MIN_HEADER_SIZE = 12;
public static final int MIN_SEQUENCE_NUMBER = 0;
public static final int MAX_SEQUENCE_NUMBER = 0xFFFF;
public static final int CSRC_SIZE = 4;
private static final byte[] EMPTY = new byte[0];
/** The RTP version field (Word 0, bits 0-1), should always be 2. */
public final byte version = RTP_VERSION;
/** The RTP padding bit (Word 0, bit 2). */
public final boolean padding;
/** The RTP extension bit (Word 0, bit 3). */
public final boolean extension;
/** The RTP CSRC count field (Word 0, bits 4-7). */
public final byte csrcCount;
/** The RTP marker bit (Word 0, bit 8). */
public final boolean marker;
/** The RTP CSRC count field (Word 0, bits 9-15). */
public final byte payloadType;
/** The RTP sequence number field (Word 0, bits 16-31). */
public final int sequenceNumber;
/** The RTP timestamp field (Word 1). */
public final long timestamp;
/** The RTP SSRC field (Word 2). */
public final int ssrc;
/** The RTP CSRC fields (Optional, up to 15 items). */
public final byte[] csrc;
public final byte[] payloadData;
/**
* Creates an {@link RtpPacket} from a {@link ParsableByteArray}.
*
* @param packetBuffer The buffer that contains the RTP packet data.
* @return The built {@link RtpPacket}.
*/
@Nullable
public static RtpPacket parse(ParsableByteArray packetBuffer) {
if (packetBuffer.bytesLeft() < MIN_HEADER_SIZE) {
return null;
}
// Word 0.
int firstByte = packetBuffer.readUnsignedByte();
byte version = (byte) (firstByte >> 6);
boolean padding = ((firstByte >> 5) & 0x1) == 1;
byte csrcCount = (byte) (firstByte & 0xF);
if (version != RTP_VERSION) {
return null;
}
int secondByte = packetBuffer.readUnsignedByte();
boolean marker = ((secondByte >> 7) & 0x1) == 1;
byte payloadType = (byte) (secondByte & 0x7F);
int sequenceNumber = packetBuffer.readUnsignedShort();
// Word 1.
long timestamp = packetBuffer.readUnsignedInt();
// Word 2.
int ssrc = packetBuffer.readInt();
// CSRC.
byte[] csrc;
if (csrcCount > 0) {
csrc = new byte[csrcCount * CSRC_SIZE];
for (int i = 0; i < csrcCount; i++) {
packetBuffer.readBytes(csrc, i * CSRC_SIZE, CSRC_SIZE);
}
} else {
csrc = EMPTY;
}
// Everything else will be RTP payload.
byte[] payloadData = new byte[packetBuffer.bytesLeft()];
packetBuffer.readBytes(payloadData, 0, packetBuffer.bytesLeft());
Builder builder = new Builder();
return builder
.setPadding(padding)
.setMarker(marker)
.setPayloadType(payloadType)
.setSequenceNumber(sequenceNumber)
.setTimestamp(timestamp)
.setSsrc(ssrc)
.setCsrc(csrc)
.setPayloadData(payloadData)
.build();
}
/**
* Creates an {@link RtpPacket} from a byte array.
*
* @param buffer The buffer that contains the RTP packet data.
* @param length The length of the RTP packet.
* @return The built {@link RtpPacket}.
*/
@Nullable
public static RtpPacket parse(byte[] buffer, int length) {
return parse(new ParsableByteArray(buffer, length));
}
private RtpPacket(Builder builder) {
this.padding = builder.padding;
this.extension = false;
this.marker = builder.marker;
this.payloadType = builder.payloadType;
this.sequenceNumber = builder.sequenceNumber;
this.timestamp = builder.timestamp;
this.ssrc = builder.ssrc;
this.csrc = builder.csrc;
this.csrcCount = (byte) (this.csrc.length / CSRC_SIZE);
this.payloadData = builder.payloadData;
}
/**
* Writes the data in an RTP packet to a target buffer.
*
* <p>The size of the target buffer and the length argument should be big enough so that the
* entire RTP packet could fit. That is, if there is not enough space to store the entire RTP
* packet, no bytes will be written. The maximum size of an RTP packet is defined as {@link
* RtpPacket#MAX_SIZE}.
*
* @param target A target byte buffer to which the packet data is copied.
* @param offset The offset into the target array at which to write.
* @param length The maximum number of bytes that can be written.
* @return The number of bytes written, or {@link C#LENGTH_UNSET} if there is not enough space to
* write the packet.
*/
public int writeToBuffer(byte[] target, int offset, int length) {
int packetLength = MIN_HEADER_SIZE + (CSRC_SIZE * csrcCount) + payloadData.length;
if (length < packetLength || target.length - offset < packetLength) {
return C.LENGTH_UNSET;
}
ByteBuffer buffer = ByteBuffer.wrap(target, offset, length);
byte firstByte =
(byte)
((version << 6)
| ((padding ? 1 : 0) << 5)
| ((extension ? 1 : 0) << 4)
| (csrcCount & 0xF));
byte secondByte = (byte) (((marker ? 1 : 0) << 7) | (payloadType & 0x7F));
buffer
.put(firstByte)
.put(secondByte)
.putShort((short) sequenceNumber)
.putInt((int) timestamp)
.putInt(ssrc)
.put(csrc)
.put(payloadData);
return packetLength;
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
RtpPacket rtpPacket = (RtpPacket) o;
return payloadType == rtpPacket.payloadType
&& sequenceNumber == rtpPacket.sequenceNumber
&& marker == rtpPacket.marker
&& timestamp == rtpPacket.timestamp
&& ssrc == rtpPacket.ssrc;
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + payloadType;
result = 31 * result + sequenceNumber;
result = 31 * result + (marker ? 1 : 0);
result = 31 * result + (int) (timestamp ^ (timestamp >>> 32));
result = 31 * result + ssrc;
return result;
}
@Override
public String toString() {
return Util.formatInvariant(
"RtpPacket(payloadType=%d, seq=%d, timestamp=%d, ssrc=%x, marker=%b)",
payloadType, sequenceNumber, timestamp, ssrc, marker);
}
}

View File

@ -0,0 +1,201 @@
/*
* Copyright 2020 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 com.google.android.exoplayer2.source.rtsp.rtp;
import static java.lang.Math.abs;
import static java.lang.Math.max;
import static java.lang.Math.min;
import androidx.annotation.GuardedBy;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.google.android.exoplayer2.C;
import java.util.TreeSet;
/**
* Orders RTP packets by their sequence numbers to correct the possible alternation in packet
* ordering, introduced by UDP transport.
*/
/* package */ final class RtpPacketReorderingQueue {
/** The maximum sequence number discontinuity allowed without resetting the re-ordering buffer. */
@VisibleForTesting /* package */ static final int MAX_SEQUENCE_LEAP_ALLOWED = 1000;
private static final int MAX_SEQUENCE_NUMBER = RtpPacket.MAX_SEQUENCE_NUMBER;
// Use set to eliminate duplicating packets.
// TODO(b/172331505) Set a upper limit on packetQueue to mitigate out of memory error.
@GuardedBy("this")
private final TreeSet<RtpPacketContainer> packetQueue;
@GuardedBy("this")
private int lastReceivedSequenceNumber;
@GuardedBy("this")
private int lastDequeuedSequenceNumber;
@GuardedBy("this")
private boolean started;
/** Creates an instance. */
public RtpPacketReorderingQueue() {
packetQueue =
new TreeSet<>(
(packetContainer1, packetContainer2) ->
calculateSequenceNumberShift(
packetContainer1.packet.sequenceNumber,
packetContainer2.packet.sequenceNumber));
reset();
}
public synchronized void reset() {
packetQueue.clear();
started = false;
lastDequeuedSequenceNumber = C.INDEX_UNSET;
lastReceivedSequenceNumber = C.INDEX_UNSET;
}
/**
* Offer one packet to the reordering queue.
*
* <p>A packet will not be added to the queue, if a logically preceding packet has already been
* dequeued.
*
* <p>If a packet creates a shift in sequence number that is at least {@link
* #MAX_SEQUENCE_LEAP_ALLOWED} compared to the last offered packet, the queue is emptied and then
* the packet is added.
*
* @param packet The packet to add.
* @param receivedTimestampMs The timestamp in milliseconds, at which the packet was received.
* @return Returns {@code false} if the packet was dropped because it was outside the expected
* range of accepted packets, otherwise {@code true} (on duplicated packets, this method
* returns {@code true}).
*/
public synchronized boolean offer(RtpPacket packet, long receivedTimestampMs) {
int packetSequenceNumber = packet.sequenceNumber;
if (!started) {
reset();
lastDequeuedSequenceNumber = prevSequenceNumber(packetSequenceNumber);
started = true;
addToQueue(new RtpPacketContainer(packet, receivedTimestampMs));
return true;
}
int expectedSequenceNumber = nextSequenceNumber(lastReceivedSequenceNumber);
// A positive shift means the packet succeeds the last received packet.
int sequenceNumberShift =
calculateSequenceNumberShift(packetSequenceNumber, expectedSequenceNumber);
if (abs(sequenceNumberShift) < MAX_SEQUENCE_LEAP_ALLOWED) {
if (calculateSequenceNumberShift(packetSequenceNumber, lastDequeuedSequenceNumber) > 0) {
// Add the packet in the queue only if a succeeding packet has not been dequeued already.
addToQueue(new RtpPacketContainer(packet, receivedTimestampMs));
return true;
}
} else {
// Discard all previous received packets and start subsequent receiving from here.
lastDequeuedSequenceNumber = prevSequenceNumber(packetSequenceNumber);
packetQueue.clear();
addToQueue(new RtpPacketContainer(packet, receivedTimestampMs));
return true;
}
return false;
}
/**
* Polls an {@link RtpPacket} from the queue.
*
* @param cutoffTimestampMs A cutoff timestamp in milliseconds used to determine if the head of
* the queue should be dequeued, even if it's not the next packet in sequence.
* @return Returns a packet if the packet at the queue head is the next packet in sequence; or its
* {@link #offer received} timestamp is before {@code cutoffTimestampMs}. Otherwise {@code
* null}.
*/
@Nullable
public synchronized RtpPacket poll(long cutoffTimestampMs) {
if (packetQueue.isEmpty()) {
return null;
}
RtpPacketContainer packetContainer = packetQueue.first();
int packetSequenceNumber = packetContainer.packet.sequenceNumber;
if (packetSequenceNumber == nextSequenceNumber(lastDequeuedSequenceNumber)
|| cutoffTimestampMs >= packetContainer.receivedTimestampMs) {
packetQueue.pollFirst();
lastDequeuedSequenceNumber = packetSequenceNumber;
return packetContainer.packet;
}
return null;
}
// Internals.
private synchronized void addToQueue(RtpPacketContainer packet) {
lastReceivedSequenceNumber = packet.packet.sequenceNumber;
packetQueue.add(packet);
}
private static final class RtpPacketContainer {
public final RtpPacket packet;
public final long receivedTimestampMs;
/** Creates an instance. */
public RtpPacketContainer(RtpPacket packet, long receivedTimestampMs) {
this.packet = packet;
this.receivedTimestampMs = receivedTimestampMs;
}
}
private static int nextSequenceNumber(int sequenceNumber) {
return (sequenceNumber + 1) % MAX_SEQUENCE_NUMBER;
}
private static int prevSequenceNumber(int sequenceNumber) {
return sequenceNumber == 0
? MAX_SEQUENCE_NUMBER - 1
: (sequenceNumber - 1) % MAX_SEQUENCE_NUMBER;
}
/**
* Calculates the sequence number shift, accounting for wrapping around.
*
* @param sequenceNumber The currently received sequence number.
* @param previousSequenceNumber The previous sequence number to compare against.
* @return The shift in the sequence numbers. A positive shift indicates that {@code
* sequenceNumber} is logically after {@code previousSequenceNumber}, whereas a negative shift
* means that {@code sequenceNumber} is logically before {@code previousSequenceNumber}.
*/
private static int calculateSequenceNumberShift(int sequenceNumber, int previousSequenceNumber) {
int sequenceShift = sequenceNumber - previousSequenceNumber;
if (abs(sequenceShift) > MAX_SEQUENCE_LEAP_ALLOWED) {
int shift =
min(sequenceNumber, previousSequenceNumber)
- max(sequenceNumber, previousSequenceNumber)
+ MAX_SEQUENCE_NUMBER;
// Check whether this is actually an wrap-over. For example, it is a wrap around if receiving
// 65500 (prevSequenceNumber) after 1 (sequenceNumber); but it is not when prevSequenceNumber
// is 30000.
if (shift < MAX_SEQUENCE_LEAP_ALLOWED) {
return sequenceNumber < previousSequenceNumber
? /* receiving 65000 (curr) then 1 (prev) */ shift
: /* receiving 1 (curr) then 65500 (prev) */ -shift;
}
}
return sequenceShift;
}
}

View File

@ -0,0 +1,127 @@
/*
* 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 com.google.android.exoplayer2.source.rtsp.rtp;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.source.rtsp.sdp.MediaDescription;
import com.google.android.exoplayer2.source.rtsp.sdp.SessionDescription;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.common.base.Ascii;
import com.google.common.collect.ImmutableMap;
import java.util.Map;
/**
* Represents the payload format used in RTP.
*
* <p>In RTSP playback, the format information is always present in the {@link SessionDescription}
* enclosed in the response of a DESCRIBE request. Within each track's {@link MediaDescription}, it
* is the attributes FMTP and RTPMAP that allows us to recreate the media format.
*
* <p>This class wraps around the {@link Format} class, in addition to the instance fields that are
* specific to RTP.
*/
public final class RtpPayloadFormat {
private static final String RTP_MEDIA_AC3 = "AC3";
private static final String RTP_MEDIA_MPEG4_GENERIC = "MPEG4-GENERIC";
private static final String RTP_MEDIA_H264 = "H264";
/** Returns whether the format of a {@link MediaDescription} is supported. */
public static boolean isFormatSupported(MediaDescription mediaDescription) {
switch (Ascii.toUpperCase(mediaDescription.rtpMapAttribute.mediaEncoding)) {
case RTP_MEDIA_AC3:
case RTP_MEDIA_H264:
case RTP_MEDIA_MPEG4_GENERIC:
return true;
default:
return false;
}
}
/**
* Gets the MIME type that is associated with the RTP media type.
*
* <p>For instance, RTP media type "H264" maps to {@link MimeTypes#VIDEO_H264}.
*
* @throws IllegalArgumentException When the media type is not supported/recognized.
*/
public static String getMimeTypeFromRtpMediaType(String mediaType) {
switch (Ascii.toUpperCase(mediaType)) {
case RTP_MEDIA_AC3:
return MimeTypes.AUDIO_AC3;
case RTP_MEDIA_H264:
return MimeTypes.VIDEO_H264;
case RTP_MEDIA_MPEG4_GENERIC:
return MimeTypes.AUDIO_AAC;
default:
throw new IllegalArgumentException(mediaType);
}
}
/** The payload type associated with this format. */
public final int rtpPayloadType;
/** The clock rate in Hertz, associated with the format. */
public final int clockRate;
/** The {@link Format} of this RTP payload. */
public final Format format;
/** The format parameters, mapped from the SDP FMTP attribute (RFC2327 Page 22). */
public final ImmutableMap<String, String> fmtpParameters;
/**
* Creates a new instance.
*
* @param format The associated {@link Format media format}.
* @param rtpPayloadType The assigned RTP payload type, from the RTPMAP attribute in {@link
* MediaDescription}.
* @param clockRate The associated clock rate in hertz.
* @param fmtpParameters The format parameters, from the SDP FMTP attribute (RFC2327 Page 22),
* empty if unset. The keys and values are specified in the RFCs for specific formats. For
* instance, RFC3640 Section 4.1 defines keys like profile-level-id and config.
*/
public RtpPayloadFormat(
Format format, int rtpPayloadType, int clockRate, Map<String, String> fmtpParameters) {
this.rtpPayloadType = rtpPayloadType;
this.clockRate = clockRate;
this.format = format;
this.fmtpParameters = ImmutableMap.copyOf(fmtpParameters);
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
RtpPayloadFormat that = (RtpPayloadFormat) o;
return rtpPayloadType == that.rtpPayloadType
&& clockRate == that.clockRate
&& format.equals(that.format)
&& fmtpParameters.equals(that.fmtpParameters);
}
@Override
public int hashCode() {
int result = 7;
result = 31 * result + rtpPayloadType;
result = 31 * result + clockRate;
result = 31 * result + format.hashCode();
result = 31 * result + fmtpParameters.hashCode();
return result;
}
}

View File

@ -0,0 +1,19 @@
/*
* Copyright 2020 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.
*/
@NonNullApi
package com.google.android.exoplayer2.source.rtsp.rtp;
import com.google.android.exoplayer2.util.NonNullApi;

View File

@ -0,0 +1,43 @@
/*
* Copyright 2020 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 com.google.android.exoplayer2.source.rtsp.rtp.reader;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.source.rtsp.rtp.RtpPayloadFormat;
import com.google.android.exoplayer2.util.MimeTypes;
/** Default {@link RtpPayloadReader.Factory} implementation. */
public final class DefaultRtpPayloadReaderFactory implements RtpPayloadReader.Factory {
@Override
@Nullable
public RtpPayloadReader createPayloadReader(RtpPayloadFormat payloadFormat) {
switch (checkNotNull(payloadFormat.format.sampleMimeType)) {
case MimeTypes.AUDIO_AC3:
return new RtpAc3Reader(payloadFormat);
case MimeTypes.AUDIO_AAC:
return new RtpAacReader(payloadFormat);
case MimeTypes.VIDEO_H264:
return new RtpH264Reader(payloadFormat);
default:
// No supported reader, returning null.
}
return null;
}
}

View File

@ -0,0 +1,166 @@
/*
* Copyright 2020 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 com.google.android.exoplayer2.source.rtsp.rtp.reader;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.source.rtsp.rtp.RtpPayloadFormat;
import com.google.android.exoplayer2.util.ParsableBitArray;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
import com.google.common.base.Ascii;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* Parses a AAC byte stream carried on RTP packets and extracts individual samples. Interleaving
* mode is not supported.
*/
/* package */ final class RtpAacReader implements RtpPayloadReader {
/** AAC low bit rate mode, RFC3640 Section 3.3.5. */
private static final String AAC_LOW_BITRATE_MODE = "AAC-lbr";
/** AAC high bit rate mode, RFC3640 Section 3.3.6. */
private static final String AAC_HIGH_BITRATE_MODE = "AAC-hbr";
private static final String TAG = "RtpAacReader";
private final RtpPayloadFormat payloadFormat;
private final ParsableBitArray auHeaderScratchBit;
private final int sampleRate;
private final int auSizeFieldBitSize;
private final int auIndexFieldBitSize;
private final int numBitsInAuHeader;
private long firstReceivedTimestamp;
private @MonotonicNonNull TrackOutput trackOutput;
private long startTimeOffsetUs;
public RtpAacReader(RtpPayloadFormat payloadFormat) {
this.payloadFormat = payloadFormat;
this.auHeaderScratchBit = new ParsableBitArray();
this.sampleRate = this.payloadFormat.clockRate;
// mode attribute is mandatory. See RFC3640 Section 4.1.
String mode = checkNotNull(payloadFormat.fmtpParameters.get("mode"));
if (Ascii.equalsIgnoreCase(mode, AAC_HIGH_BITRATE_MODE)) {
auSizeFieldBitSize = 13;
auIndexFieldBitSize = 3;
} else if (Ascii.equalsIgnoreCase(mode, AAC_LOW_BITRATE_MODE)) {
auSizeFieldBitSize = 6;
auIndexFieldBitSize = 2;
} else {
throw new UnsupportedOperationException("AAC mode not supported");
}
// TODO(b/172331505) Add support for other AU-Header fields, like CTS-flag, CTS-delta, etc.
numBitsInAuHeader = auIndexFieldBitSize + auSizeFieldBitSize;
}
// RtpPayloadReader implementation.
@Override
public void createTracks(ExtractorOutput extractorOutput, int trackId) {
trackOutput = extractorOutput.track(trackId, C.TRACK_TYPE_AUDIO);
trackOutput.format(payloadFormat.format);
}
@Override
public void onReceivingFirstPacket(long timestamp, int sequenceNumber) {
this.firstReceivedTimestamp = timestamp;
}
@Override
public void consume(
ParsableByteArray data, long timestamp, int sequenceNumber, boolean isFrameBoundary) {
/*
AAC as RTP payload (RFC3640):
+---------+-----------+-----------+---------------+
| RTP | AU Header | Auxiliary | Access Unit |
| Header | Section | Section | Data Section |
+---------+-----------+-----------+---------------+
<----------RTP Packet Payload----------->
Access Unit(AU) Header section
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- .. -+-+-+-+-+-+-+-+-+-+
|AU-headers-length|AU-header|AU-header| |AU-header|padding|
|in bits | (1) | (2) | | (n) | bits |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- .. -+-+-+-+-+-+-+-+-+-+
The 16-bit AU-headers-length is mandatory in the AAC-lbr and AAC-hbr modes that we support.
*/
checkNotNull(trackOutput);
// Reads AU-header-length that specifies the length in bits of the immediately following
// AU-headers, excluding the padding.
int auHeadersBitLength = data.readShort();
int auHeaderCount = auHeadersBitLength / numBitsInAuHeader;
long sampleTimeUs =
toSampleTimeUs(startTimeOffsetUs, timestamp, firstReceivedTimestamp, sampleRate);
// Points to the start of the AU-headers (right past the AU-headers-length).
auHeaderScratchBit.reset(data);
if (auHeaderCount == 1) {
// Reads the first AU-Header that contains AU-Size and AU-Index/AU-Index-delta.
int auSize = auHeaderScratchBit.readBits(auSizeFieldBitSize);
auHeaderScratchBit.skipBits(auIndexFieldBitSize);
// Outputs all the received data, whether fragmented or not.
trackOutput.sampleData(data, data.bytesLeft());
if (isFrameBoundary) {
outputSampleMetadata(trackOutput, sampleTimeUs, auSize);
}
} else {
// Skips the AU-headers section to the data section, accounts for the possible padding bits.
data.skipBytes((auHeadersBitLength + 7) / 8);
for (int i = 0; i < auHeaderCount; i++) {
int auSize = auHeaderScratchBit.readBits(auSizeFieldBitSize);
auHeaderScratchBit.skipBits(auIndexFieldBitSize);
trackOutput.sampleData(data, auSize);
outputSampleMetadata(trackOutput, sampleTimeUs, auSize);
// The sample time of the of the i-th AU (RFC3640 Page 17):
// (timestamp-of-the-first-AU) + i * (access-unit-duration)
sampleTimeUs +=
Util.scaleLargeTimestamp(
auHeaderCount, /* multiplier= */ C.MICROS_PER_SECOND, /* divisor= */ sampleRate);
}
}
}
@Override
public void seek(long nextRtpTimestamp, long timeUs) {
firstReceivedTimestamp = nextRtpTimestamp;
startTimeOffsetUs = timeUs;
}
// Internal methods.
private static void outputSampleMetadata(TrackOutput trackOutput, long sampleTimeUs, int size) {
trackOutput.sampleMetadata(
sampleTimeUs, C.BUFFER_FLAG_KEY_FRAME, size, /* offset= */ 0, /* encryptionData= */ null);
}
/** Returns the correct sample time from RTP timestamp, accounting for the AAC sampling rate. */
private static long toSampleTimeUs(
long startTimeOffsetUs, long rtpTimestamp, long firstReceivedRtpTimestamp, int sampleRate) {
return startTimeOffsetUs
+ Util.scaleLargeTimestamp(
rtpTimestamp - firstReceivedRtpTimestamp,
/* multiplier= */ C.MICROS_PER_SECOND,
/* divisor= */ sampleRate);
}
}

View File

@ -0,0 +1,219 @@
/*
* 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 com.google.android.exoplayer2.source.rtsp.rtp.reader;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.android.exoplayer2.util.Assertions.checkState;
import static com.google.android.exoplayer2.util.Util.castNonNull;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.audio.Ac3Util;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.source.rtsp.rtp.RtpPayloadFormat;
import com.google.android.exoplayer2.util.ParsableBitArray;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** Parses an AC3 byte stream carried on RTP packets, and extracts AC3 frames. */
public final class RtpAc3Reader implements RtpPayloadReader {
/** AC3 frame types defined in RFC4184 Section 4.1.1. */
private static final int AC3_FRAME_TYPE_COMPLETE_FRAME = 0;
/** Initial fragment of frame which includes the first 5/8ths of the frame. */
private static final int AC3_FRAME_TYPE_INITIAL_FRAGMENT_A = 1;
/** Initial fragment of frame which does not include the first 5/8ths of the frame. */
private static final int AC3_FRAME_TYPE_INITIAL_FRAGMENT_B = 2;
private static final int AC3_FRAME_TYPE_NON_INITIAL_FRAGMENT = 3;
/** AC3 payload header size in bytes. */
private static final int AC3_PAYLOAD_HEADER_SIZE = 2;
private final RtpPayloadFormat payloadFormat;
private final ParsableBitArray scratchBitBuffer;
private @MonotonicNonNull TrackOutput trackOutput;
private int numBytesPendingMetadataOutput;
private long firstReceivedTimestamp;
private long sampleTimeUsOfFramePendingMetadataOutput;
private long startTimeOffsetUs;
public RtpAc3Reader(RtpPayloadFormat payloadFormat) {
this.payloadFormat = payloadFormat;
scratchBitBuffer = new ParsableBitArray();
firstReceivedTimestamp = C.TIME_UNSET;
}
@Override
public void createTracks(ExtractorOutput extractorOutput, int trackId) {
trackOutput = extractorOutput.track(trackId, C.TRACK_TYPE_AUDIO);
trackOutput.format(payloadFormat.format);
}
@Override
public void onReceivingFirstPacket(long timestamp, int sequenceNumber) {
checkState(firstReceivedTimestamp == C.TIME_UNSET);
firstReceivedTimestamp = timestamp;
}
@Override
public void consume(
ParsableByteArray data, long timestamp, int sequenceNumber, boolean isFrameBoundary) {
/*
AC-3 payload as an RTP payload (RFC4184).
+-+-+-+-+-+-+-+-+-+-+-+-+-+- .. +-+-+-+-+-+-+-+
| Payload | Frame | Frame | | Frame |
| Header | (1) | (2) | | (n) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+- .. +-+-+-+-+-+-+-+
The payload header:
0 1
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| MBZ | FT| NF |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
FT: frame type.
NF: number of frames/fragments.
*/
int frameType = data.readUnsignedByte() & 0x3;
int numOfFrames = data.readUnsignedByte() & 0xFF;
long sampleTimeUs =
toSampleTimeUs(
startTimeOffsetUs, timestamp, firstReceivedTimestamp, payloadFormat.clockRate);
switch (frameType) {
case AC3_FRAME_TYPE_COMPLETE_FRAME:
maybeOutputSampleMetadata();
if (numOfFrames == 1) {
// Single AC3 frame in one RTP packet.
processSingleFramePacket(data, sampleTimeUs);
} else {
// Multiple AC3 frames in one RTP packet.
processMultiFramePacket(data, numOfFrames, sampleTimeUs);
}
break;
case AC3_FRAME_TYPE_INITIAL_FRAGMENT_A:
case AC3_FRAME_TYPE_INITIAL_FRAGMENT_B:
maybeOutputSampleMetadata();
// Falls through.
case AC3_FRAME_TYPE_NON_INITIAL_FRAGMENT:
// The content of an AC3 frame is split into multiple RTP packets.
processFragmentedPacket(data, isFrameBoundary, frameType, sampleTimeUs);
break;
default:
throw new IllegalArgumentException(String.valueOf(frameType));
}
}
@Override
public void seek(long nextRtpTimestamp, long timeUs) {
firstReceivedTimestamp = nextRtpTimestamp;
startTimeOffsetUs = timeUs;
}
private void processSingleFramePacket(ParsableByteArray data, long sampleTimeUs) {
int frameSize = data.bytesLeft();
checkNotNull(trackOutput).sampleData(data, frameSize);
castNonNull(trackOutput)
.sampleMetadata(
/* timeUs= */ sampleTimeUs,
/* flags= */ C.BUFFER_FLAG_KEY_FRAME,
/* size= */ frameSize,
/* offset= */ 0,
/* encryptionData= */ null);
}
private void processMultiFramePacket(ParsableByteArray data, int numOfFrames, long sampleTimeUs) {
// The size of each frame must be obtained by reading AC3 sync frame.
scratchBitBuffer.reset(data.getData());
// Move the read location after the AC3 payload header.
scratchBitBuffer.skipBytes(AC3_PAYLOAD_HEADER_SIZE);
for (int i = 0; i < numOfFrames; i++) {
Ac3Util.SyncFrameInfo frameInfo = Ac3Util.parseAc3SyncframeInfo(scratchBitBuffer);
checkNotNull(trackOutput).sampleData(data, frameInfo.frameSize);
castNonNull(trackOutput)
.sampleMetadata(
/* timeUs= */ sampleTimeUs,
/* flags= */ C.BUFFER_FLAG_KEY_FRAME,
/* size= */ frameInfo.frameSize,
/* offset= */ 0,
/* encryptionData= */ null);
sampleTimeUs += (frameInfo.sampleCount / frameInfo.sampleRate) * C.MICROS_PER_SECOND;
// Advance the position by the number of bytes read.
scratchBitBuffer.skipBytes(frameInfo.frameSize);
}
}
private void processFragmentedPacket(
ParsableByteArray data, boolean isFrameBoundary, int frameType, long sampleTimeUs) {
int bytesToWrite = data.bytesLeft();
checkNotNull(trackOutput).sampleData(data, bytesToWrite);
numBytesPendingMetadataOutput += bytesToWrite;
sampleTimeUsOfFramePendingMetadataOutput = sampleTimeUs;
if (isFrameBoundary && frameType == AC3_FRAME_TYPE_NON_INITIAL_FRAGMENT) {
// Last RTP packet in the series of fragmentation packets.
outputSampleMetadataForFragmentedPackets();
}
}
/**
* Checks and outputs sample metadata, if the last packet of a series of fragmented packets is
* lost.
*
* <p>Call this method only when receiving an initial packet, i.e. on packets with type
*
* <ul>
* <li>{@link #AC3_FRAME_TYPE_COMPLETE_FRAME},
* <li>{@link #AC3_FRAME_TYPE_INITIAL_FRAGMENT_A}, or
* <li>{@link #AC3_FRAME_TYPE_INITIAL_FRAGMENT_B}.
* </ul>
*/
private void maybeOutputSampleMetadata() {
if (numBytesPendingMetadataOutput > 0) {
outputSampleMetadataForFragmentedPackets();
}
}
private void outputSampleMetadataForFragmentedPackets() {
castNonNull(trackOutput)
.sampleMetadata(
/* timeUs= */ sampleTimeUsOfFramePendingMetadataOutput,
/* flags= */ C.BUFFER_FLAG_KEY_FRAME,
/* size= */ numBytesPendingMetadataOutput,
/* offset= */ 0,
/* encryptionData= */ null);
numBytesPendingMetadataOutput = 0;
}
/** Returns the correct sample time from RTP timestamp, accounting for the AC3 sampling rate. */
private static long toSampleTimeUs(
long startTimeOffsetUs, long rtpTimestamp, long firstReceivedRtpTimestamp, int sampleRate) {
return startTimeOffsetUs
+ Util.scaleLargeTimestamp(
rtpTimestamp - firstReceivedRtpTimestamp,
/* multiplier= */ C.MICROS_PER_SECOND,
/* divisor= */ sampleRate);
}
}

View File

@ -0,0 +1,307 @@
/*
* Copyright 2020 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 com.google.android.exoplayer2.source.rtsp.rtp.reader;
import static com.google.android.exoplayer2.util.Assertions.checkStateNotNull;
import static com.google.android.exoplayer2.util.Util.castNonNull;
import android.util.Log;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.source.rtsp.rtp.RtpPacket;
import com.google.android.exoplayer2.source.rtsp.rtp.RtpPayloadFormat;
import com.google.android.exoplayer2.util.NalUnitUtil;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
/** Parses an H264 byte stream carried on RTP packets, and extracts H264 Access Units. */
/* package */ final class RtpH264Reader implements RtpPayloadReader {
private static final String TAG = "RtpH264Reader";
// TODO(b/172331505) Move NAL related constants to NalUnitUtil.
private static final ParsableByteArray NAL_START_CODE =
new ParsableByteArray(NalUnitUtil.NAL_START_CODE);
private static final int NAL_START_CODE_LENGTH = NalUnitUtil.NAL_START_CODE.length;
private static final long MEDIA_CLOCK_FREQUENCY = 90_000;
/** Offset of payload data within a FU type A payload. */
private static final int FU_PAYLOAD_OFFSET = 2;
/** Single Time Aggregation Packet type A. */
private static final int RTP_PACKET_TYPE_STAP_A = 24;
/** Fragmentation Unit type A. */
private static final int RTP_PACKET_TYPE_FU_A = 28;
/** IDR NAL unit type. */
private static final int NAL_UNIT_TYPE_IDR = 5;
/** Scratch for Fragmentation Unit RTP packets. */
private final ParsableByteArray fuScratchBuffer;
private final RtpPayloadFormat payloadFormat;
private @MonotonicNonNull TrackOutput trackOutput;
@C.BufferFlags private int bufferFlags;
private long firstReceivedTimestamp;
private int previousSequenceNumber;
/** The combined size of a sample that is fragmented into multiple RTP packets. */
private int fragmentedSampleSizeBytes;
private long startTimeOffsetUs;
/** Creates an instance. */
public RtpH264Reader(RtpPayloadFormat payloadFormat) {
this.payloadFormat = payloadFormat;
fuScratchBuffer = new ParsableByteArray();
firstReceivedTimestamp = C.TIME_UNSET;
previousSequenceNumber = C.INDEX_UNSET;
}
@Override
public void createTracks(ExtractorOutput extractorOutput, int trackId) {
trackOutput = extractorOutput.track(trackId, C.TRACK_TYPE_VIDEO);
castNonNull(trackOutput).format(payloadFormat.format);
}
@Override
public void onReceivingFirstPacket(long timestamp, int sequenceNumber) {}
@Override
public void consume(
ParsableByteArray data, long timestamp, int sequenceNumber, boolean isAuBoundary)
throws ParserException {
int rtpH264PacketMode;
try {
// RFC6184 Section 5.6, 5.7 and 5.8.
rtpH264PacketMode = data.getData()[0] & 0x1F;
} catch (IndexOutOfBoundsException e) {
throw new ParserException(e);
}
checkStateNotNull(trackOutput);
if (rtpH264PacketMode > 0 && rtpH264PacketMode < 24) {
processSingleNalUnitPacket(data);
} else if (rtpH264PacketMode == RTP_PACKET_TYPE_STAP_A) {
processSingleTimeAggregationPacket(data);
} else if (rtpH264PacketMode == RTP_PACKET_TYPE_FU_A) {
processFragmentationUnitPacket(data, sequenceNumber);
} else {
throw new ParserException(
String.format("RTP H264 packetization mode [%d] not supported.", rtpH264PacketMode));
}
if (isAuBoundary) {
if (firstReceivedTimestamp == C.TIME_UNSET) {
firstReceivedTimestamp = timestamp;
}
long timeUs = toSampleUs(startTimeOffsetUs, timestamp, firstReceivedTimestamp);
trackOutput.sampleMetadata(
timeUs,
bufferFlags,
fragmentedSampleSizeBytes,
/* offset= */ 0,
/* encryptionData= */ null);
fragmentedSampleSizeBytes = 0;
}
previousSequenceNumber = sequenceNumber;
}
@Override
public void seek(long nextRtpTimestamp, long timeUs) {
firstReceivedTimestamp = nextRtpTimestamp;
fragmentedSampleSizeBytes = 0;
startTimeOffsetUs = timeUs;
}
// Internal methods.
/**
* Processes Single NAL Unit packet (RFC6184 Section 5.6).
*
* <p>Outputs the single NAL Unit (with start code prepended) to {@link #trackOutput}. Sets {@link
* #bufferFlags} and {@link #fragmentedSampleSizeBytes} accordingly.
*/
@RequiresNonNull("trackOutput")
private void processSingleNalUnitPacket(ParsableByteArray data) {
// Example of a Single Nal Unit packet
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// |F|NRI| Type | |
// +-+-+-+-+-+-+-+-+ |
// | |
// | Bytes 2..n of a single NAL unit |
// | |
// | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | :...OPTIONAL RTP padding |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
int numBytesInData = data.bytesLeft();
fragmentedSampleSizeBytes += writeStartCode(trackOutput);
trackOutput.sampleData(data, numBytesInData);
fragmentedSampleSizeBytes += numBytesInData;
int nalHeaderType = data.getData()[0] & 0x1F;
bufferFlags = getBufferFlagsFromNalType(nalHeaderType);
}
/**
* Processes STAP Type A packet (RFC6184 Section 5.7).
*
* <p>Outputs the received aggregation units (with start code prepended) to {@link #trackOutput}.
* Sets {@link #bufferFlags} and {@link #fragmentedSampleSizeBytes} accordingly.
*/
@RequiresNonNull("trackOutput")
private void processSingleTimeAggregationPacket(ParsableByteArray data) {
// Example of an STAP-A packet.
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | RTP Header |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// |STAP-A NAL HDR | NALU 1 Size | NALU 1 HDR |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | NALU 1 Data |
// : :
// + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | | NALU 2 Size | NALU 2 HDR |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | NALU 2 Data |
// : :
// | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | :...OPTIONAL RTP padding |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// Skips STAP-A NAL HDR that has the NAL format |F|NRI|Type|, but with Type replaced by the
// STAP-A type id (RTP_PACKET_TYPE_STAP_A).
data.readUnsignedByte();
// Gets all NAL units until the remaining bytes are only enough to store an RTP padding.
int nalUnitLength;
while (data.bytesLeft() > 4) {
nalUnitLength = data.readUnsignedShort();
fragmentedSampleSizeBytes += writeStartCode(trackOutput);
trackOutput.sampleData(data, nalUnitLength);
fragmentedSampleSizeBytes += nalUnitLength;
}
// Treat Aggregated NAL units as non key frames.
// TODO(internal b/172331505) examine whether STAP mode carries keyframes.
bufferFlags = 0;
}
/**
* Processes Fragmentation Unit Type A packet (RFC6184 Section 5.8).
*
* <p>This method will be invoked multiple times to receive a single frame that is broken down
* into a series of fragmentation units in multiple RTP packets.
*
* <p>Outputs the received fragmentation units (with start code prepended) to {@link
* #trackOutput}. Sets {@link #bufferFlags} and {@link #fragmentedSampleSizeBytes} accordingly.
*/
@RequiresNonNull("trackOutput")
private void processFragmentationUnitPacket(ParsableByteArray data, int packetSequenceNumber) {
// FU-A mode packet layout.
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | FU indicator | FU header | |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
// | |
// | FU payload |
// | |
// | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | :...OPTIONAL RTP padding |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
//
// FU Indicator FU Header
// 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// |F|NRI| Type |S|E|R| Type |
// +---------------+---------------+
// Indicator: Upper 3 bits are the same as NALU header, Type = 28 (FU-A type).
// Header: Start/End/Reserved/Type. Type is same as NALU type.
int fuIndicator = data.getData()[0];
int fuHeader = data.getData()[1];
int nalHeader = (fuIndicator & 0xE0) | (fuHeader & 0x1F);
boolean isFirstFuPacket = (fuHeader & 0x80) > 0;
boolean isLastFuPacket = (fuHeader & 0x40) > 0;
if (isFirstFuPacket) {
// Prepends starter code.
fragmentedSampleSizeBytes += writeStartCode(trackOutput);
// The bytes needed is 1 (NALU header) + payload size. The original data array has size 2 (FU
// indicator/header) + payload size. Thus setting the correct header and set position to 1.
data.getData()[1] = (byte) nalHeader;
fuScratchBuffer.reset(data.getData());
fuScratchBuffer.setPosition(1);
} else {
// Check that this packet is in the sequence of the previous packet.
int expectedSequenceNumber = (previousSequenceNumber + 1) % RtpPacket.MAX_SEQUENCE_NUMBER;
if (packetSequenceNumber != expectedSequenceNumber) {
Log.w(
TAG,
Util.formatInvariant(
"Received RTP packet with unexpected sequence number. Expected: %d; received: %d."
+ " Dropping packet.",
expectedSequenceNumber, packetSequenceNumber));
return;
}
// Setting position to ignore FU indicator and header.
fuScratchBuffer.reset(data.getData());
fuScratchBuffer.setPosition(FU_PAYLOAD_OFFSET);
}
int fragmentSize = fuScratchBuffer.bytesLeft();
trackOutput.sampleData(fuScratchBuffer, fragmentSize);
fragmentedSampleSizeBytes += fragmentSize;
if (isLastFuPacket) {
bufferFlags = getBufferFlagsFromNalType(nalHeader & 0x1F);
}
}
private static long toSampleUs(
long startTimeOffsetUs, long rtpTimestamp, long firstReceivedRtpTimestamp) {
return startTimeOffsetUs
+ Util.scaleLargeTimestamp(
(rtpTimestamp - firstReceivedRtpTimestamp),
/* multiplier= */ C.MICROS_PER_SECOND,
/* divisor= */ MEDIA_CLOCK_FREQUENCY);
}
private static int writeStartCode(TrackOutput trackOutput) {
trackOutput.sampleData(NAL_START_CODE, NAL_START_CODE_LENGTH);
NAL_START_CODE.setPosition(/* position= */ 0);
return NAL_START_CODE_LENGTH;
}
@C.BufferFlags
private static int getBufferFlagsFromNalType(int nalType) {
return nalType == NAL_UNIT_TYPE_IDR ? C.BUFFER_FLAG_KEY_FRAME : 0;
}
}

View File

@ -0,0 +1,85 @@
/*
* Copyright 2020 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 com.google.android.exoplayer2.source.rtsp.rtp.reader;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.source.rtsp.rtp.RtpPayloadFormat;
import com.google.android.exoplayer2.util.ParsableByteArray;
import org.checkerframework.checker.nullness.qual.Nullable;
/** Extracts media samples from the payload of received RTP packets. */
public interface RtpPayloadReader {
/** Factory of {@link RtpPayloadReader} instances. */
interface Factory {
/**
* Returns a {@link RtpPayloadReader} for a given {@link RtpPayloadFormat}.
*
* @param payloadFormat The {@link RtpPayloadFormat} of the RTP stream.
* @return A {@link RtpPayloadReader} for the packet stream, or {@code null} if the stream
* format is not supported.
*/
@Nullable
RtpPayloadReader createPayloadReader(RtpPayloadFormat payloadFormat);
}
/**
* Initializes the reader by providing its output and track id.
*
* @param extractorOutput The {@link ExtractorOutput} instance that receives the extracted data.
* @param trackId The track identifier to set on the format.
*/
void createTracks(ExtractorOutput extractorOutput, int trackId);
/**
* This method should be called on reading the first packet in a stream of incoming packets.
*
* @param timestamp The timestamp associated with the first received RTP packet. This number has
* no unit, the duration conveyed by it depends on the frequency of the media that the RTP
* packet is carrying.
* @param sequenceNumber The sequence associated with the first received RTP packet.
*/
void onReceivingFirstPacket(long timestamp, int sequenceNumber);
/**
* Consumes the payload from the an RTP packet.
*
* @param data The RTP payload to consume.
* @param timestamp The timestamp of the RTP packet that transmitted the data. This number has no
* unit, the duration conveyed by it depends on the frequency of the media that the RTP packet
* is carrying.
* @param sequenceNumber The sequence number of the RTP packet.
* @param rtpMarker The marker bit of the RTP packet. The interpretation of this bit is specific
* to each payload format.
* @throws ParserException If the data could not be parsed.
*/
void consume(ParsableByteArray data, long timestamp, int sequenceNumber, boolean rtpMarker)
throws ParserException;
/**
* Seeks the reader.
*
* <p>This method must only be invoked after the PLAY request for seeking is acknowledged by the
* RTSP server.
*
* @param nextRtpTimestamp The timestamp of the first packet to arrive after seek.
* @param timeUs The server acknowledged seek time in microseconds.
*/
void seek(long nextRtpTimestamp, long timeUs);
}

View File

@ -0,0 +1,19 @@
/*
* Copyright 2020 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.
*/
@NonNullApi
package com.google.android.exoplayer2.source.rtsp.rtp.reader;
import com.google.android.exoplayer2.util.NonNullApi;

View File

@ -0,0 +1,327 @@
/*
* 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 com.google.android.exoplayer2.source.rtsp.sdp;
import static com.google.android.exoplayer2.source.rtsp.message.RtspMessageUtil.parseInt;
import static com.google.android.exoplayer2.source.rtsp.sdp.SessionDescription.ATTR_FMTP;
import static com.google.android.exoplayer2.source.rtsp.sdp.SessionDescription.ATTR_RTPMAP;
import static com.google.android.exoplayer2.util.Assertions.checkArgument;
import static com.google.android.exoplayer2.util.Assertions.checkState;
import static com.google.android.exoplayer2.util.Util.castNonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringDef;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableMap;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/** Represents one media description section in a SDP message. */
public final class MediaDescription {
/** Represents the mandatory RTPMAP attribute in MediaDescription. Reference RFC 2327 Page 22. */
public static final class RtpMapAttribute {
/** Parses the RTPMAP attribute value (with the part "a=rtpmap:" removed). */
public static RtpMapAttribute parse(String rtpmapString) throws ParserException {
String[] rtpmapInfo = Util.split(rtpmapString, " ");
checkArgument(rtpmapInfo.length == 2);
int payloadType = parseInt(rtpmapInfo[0]);
String[] mediaInfo = Util.split(rtpmapInfo[1], "/");
checkArgument(mediaInfo.length >= 2);
int clockRate = parseInt(mediaInfo[1]);
int encodingParameters = C.INDEX_UNSET;
if (mediaInfo.length == 3) {
encodingParameters = parseInt(mediaInfo[2]);
}
return new RtpMapAttribute(
payloadType, /* mediaEncoding= */ mediaInfo[0], clockRate, encodingParameters);
}
/** The assigned RTP payload type. */
public final int payloadType;
/** The encoding method used in the RTP stream. */
public final String mediaEncoding;
/** The clock rate used in the RTP stream. */
public final int clockRate;
/** The optional encoding parameter. */
public final int encodingParameters;
private RtpMapAttribute(
int payloadType, String mediaEncoding, int clockRate, int encodingParameters) {
this.payloadType = payloadType;
this.mediaEncoding = mediaEncoding;
this.clockRate = clockRate;
this.encodingParameters = encodingParameters;
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
RtpMapAttribute that = (RtpMapAttribute) o;
return payloadType == that.payloadType
&& mediaEncoding.equals(that.mediaEncoding)
&& clockRate == that.clockRate
&& encodingParameters == that.encodingParameters;
}
@Override
public int hashCode() {
int result = 7;
result = 31 * result + payloadType;
result = 31 * result + mediaEncoding.hashCode();
result = 31 * result + clockRate;
result = 31 * result + encodingParameters;
return result;
}
}
/** Builder class for {@link MediaDescription}. */
public static final class Builder {
private final String mediaType;
private final int port;
private final String transportProtocol;
private final int payloadType;
private final ImmutableMap.Builder<String, String> attributesBuilder;
private int bitrate;
@Nullable private String mediaTitle;
@Nullable private String connection;
@Nullable private String key;
/**
* Creates a new instance.
*
* @param mediaType The media type.
* @param port The associated port number.
* @param transportProtocol The protocol used for data transport.
* @param payloadType The RTP payload type used for data transport.
*/
public Builder(String mediaType, int port, String transportProtocol, int payloadType) {
this.mediaType = mediaType;
this.port = port;
this.transportProtocol = transportProtocol;
this.payloadType = payloadType;
attributesBuilder = new ImmutableMap.Builder<>();
bitrate = Format.NO_VALUE;
}
/**
* Sets {@link MediaDescription#mediaTitle}. The default is {@code null}.
*
* @param mediaTitle The assigned media title.
* @return This builder.
*/
public Builder setMediaTitle(String mediaTitle) {
this.mediaTitle = mediaTitle;
return this;
}
/**
* Sets {@link MediaDescription#connection}. The default is {@code null}.
*
* @param connection The connection parameter.
* @return This builder.
*/
public Builder setConnection(String connection) {
this.connection = connection;
return this;
}
/**
* Sets {@link MediaDescription#bitrate}. The default is {@link Format#NO_VALUE}.
*
* @param bitrate The estimated bitrate measured in bits per second.
* @return This builder.
*/
public Builder setBitrate(int bitrate) {
this.bitrate = bitrate;
return this;
}
/**
* Sets {@link MediaDescription#key}. The default is {@code null}.
*
* @param key The encryption parameter.
* @return This builder.
*/
public Builder setKey(String key) {
this.key = key;
return this;
}
/**
* Adds an attribute entry to {@link MediaDescription#attributes}.
*
* <p>Previously added attribute under the same name will be overwritten.
*
* @param attributeName The name of the attribute.
* @param attributeValue The value of the attribute, or "" if the attribute bears no value.
* @return This builder.
*/
public Builder addAttribute(String attributeName, String attributeValue) {
attributesBuilder.put(attributeName, attributeValue);
return this;
}
/**
* Builds a new {@link MediaDescription} instance.
*
* @throws IllegalStateException When the rtpmap attribute (RFC 2327 Page 22) is not set, or
* cannot be parsed.
*/
public MediaDescription build() {
ImmutableMap<String, String> attributes = attributesBuilder.build();
try {
// rtpmap attribute is mandatory in RTSP (RFC2326 Section C.1.3).
checkState(attributes.containsKey(ATTR_RTPMAP));
RtpMapAttribute rtpMapAttribute =
RtpMapAttribute.parse(castNonNull(attributes.get(ATTR_RTPMAP)));
return new MediaDescription(this, attributes, rtpMapAttribute);
} catch (ParserException e) {
throw new IllegalStateException(e);
}
}
}
/** The media types allowed in a SDP media description. */
@Retention(RetentionPolicy.SOURCE)
@StringDef({MEDIA_TYPE_VIDEO, MEDIA_TYPE_AUDIO})
@Documented
public @interface MediaType {}
/** Audio media type. */
public static final String MEDIA_TYPE_AUDIO = "audio";
/** Video media type. */
public static final String MEDIA_TYPE_VIDEO = "video";
/** Default RTP/AVP profile. */
public static final String RTP_AVP_PROFILE = "RTP/AVP";
/** The {@link MediaType}. */
@MediaType public final String mediaType;
/** The associated port number. */
public final int port;
/** The protocol used for data transport. */
public final String transportProtocol;
/** The assigned RTP payload type. */
public final int payloadType;
/** The estimated connection bitrate in bits per second. */
public final int bitrate;
/** The assigned media title. */
@Nullable public final String mediaTitle;
// TODO(internal b/172331505) Parse the String representations into objects.
/** The connection parameters. */
@Nullable public final String connection;
/** The encryption parameter. */
@Nullable public final String key;
/** The media-specific attributes. */
public final ImmutableMap<String, String> attributes;
/** The mandatory rtpmap attribute in the media description (RFC2327 Page 22). */
public final RtpMapAttribute rtpMapAttribute;
/** Creates a new instance. */
private MediaDescription(
Builder builder, ImmutableMap<String, String> attributes, RtpMapAttribute rtpMapAttribute) {
this.mediaType = builder.mediaType;
this.port = builder.port;
this.transportProtocol = builder.transportProtocol;
this.payloadType = builder.payloadType;
this.mediaTitle = builder.mediaTitle;
this.connection = builder.connection;
this.bitrate = builder.bitrate;
this.key = builder.key;
this.attributes = attributes;
this.rtpMapAttribute = rtpMapAttribute;
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
MediaDescription other = (MediaDescription) o;
return mediaType.equals(other.mediaType)
&& port == other.port
&& transportProtocol.equals(other.transportProtocol)
&& payloadType == other.payloadType
&& bitrate == other.bitrate
&& attributes.equals(other.attributes)
&& rtpMapAttribute.equals(other.rtpMapAttribute)
&& Util.areEqual(mediaTitle, other.mediaTitle)
&& Util.areEqual(connection, other.connection)
&& Util.areEqual(key, other.key);
}
@Override
public int hashCode() {
int result = 7;
result = 31 * result + mediaType.hashCode();
result = 31 * result + port;
result = 31 * result + transportProtocol.hashCode();
result = 31 * result + payloadType;
result = 31 * result + bitrate;
result = 31 * result + attributes.hashCode();
result = 31 * result + rtpMapAttribute.hashCode();
result = 31 * result + (mediaTitle == null ? 0 : mediaTitle.hashCode());
result = 31 * result + (connection == null ? 0 : connection.hashCode());
result = 31 * result + (key == null ? 0 : key.hashCode());
return result;
}
/**
* Returns the FMTP attribute as a map of FMTP parameter names to values; or an empty map if the
* {@link MediaDescription} does not contain any FMTP attribute.
*
* <p>FMTP format reference: RFC2327 Page 27. The spaces around the FMTP attribute delimiters are
* removed. For example,
*/
public ImmutableMap<String, String> getFmtpParametersAsMap() {
@Nullable String fmtpAttributeValue = attributes.get(ATTR_FMTP);
if (fmtpAttributeValue == null) {
return ImmutableMap.of();
}
// fmtp format: RFC2327 Page 27.
String[] fmtpComponents = Util.splitAtFirst(fmtpAttributeValue, " ");
checkArgument(fmtpComponents.length == 2, fmtpAttributeValue);
// Format of the parameter: RFC3640 Section 4.4.1:
// <parameter name>=<value>[; <parameter name>=<value>].
String[] parameters = Util.split(fmtpComponents[1], ";\\s?");
ImmutableMap.Builder<String, String> formatParametersBuilder = new ImmutableMap.Builder<>();
for (String parameter : parameters) {
// The parameter values can bear equal signs, so splitAtFirst must be used.
String[] parameterPair = Util.splitAtFirst(parameter, "=");
formatParametersBuilder.put(parameterPair[0], parameterPair[1]);
}
return formatParametersBuilder.build();
}
}

View File

@ -0,0 +1,316 @@
/*
* 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 com.google.android.exoplayer2.source.rtsp.sdp;
import static com.google.android.exoplayer2.util.Util.castNonNull;
import android.net.Uri;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
/**
* Records all the information in a SDP message.
*
* <p>SDP messages encapsulate information on the media play back session, including session
* configuration information, formats of each playable track, etc. SDP is defined in RFC4566.
*/
public final class SessionDescription {
/** Builder class for {@link SessionDescription}. */
public static final class Builder {
private final ImmutableMap.Builder<String, String> attributesBuilder;
private final ImmutableList.Builder<MediaDescription> mediaDescriptionListBuilder;
private int bitrate;
@Nullable private String sessionName;
@Nullable private String origin;
@Nullable private String timing;
@Nullable private Uri uri;
@Nullable private String connection;
@Nullable private String key;
@Nullable private String sessionInfo;
@Nullable private String emailAddress;
@Nullable private String phoneNumber;
/** Creates a new instance. */
public Builder() {
attributesBuilder = new ImmutableMap.Builder<>();
mediaDescriptionListBuilder = new ImmutableList.Builder<>();
bitrate = Format.NO_VALUE;
}
/**
* Sets {@link SessionDescription#sessionName}.
*
* <p>This property must be set before calling {@link #build()}.
*
* @param sessionName The {@link SessionDescription#sessionName}.
* @return This builder.
*/
public Builder setSessionName(String sessionName) {
this.sessionName = sessionName;
return this;
}
/**
* Sets {@link SessionDescription#sessionInfo}. The default is {@code null}.
*
* @param sessionInfo The {@link SessionDescription#sessionInfo}.
* @return This builder.
*/
public Builder setSessionInfo(String sessionInfo) {
this.sessionInfo = sessionInfo;
return this;
}
/**
* Sets {@link SessionDescription#uri}. The default is {@code null}.
*
* @param uri The {@link SessionDescription#uri}.
* @return This builder.
*/
public Builder setUri(Uri uri) {
this.uri = uri;
return this;
}
/**
* Sets {@link SessionDescription#origin}.
*
* <p>This property must be set before calling {@link #build()}.
*
* @param origin The {@link SessionDescription#origin}.
* @return This builder.
*/
public Builder setOrigin(String origin) {
this.origin = origin;
return this;
}
/**
* Sets {@link SessionDescription#connection}. The default is {@code null}.
*
* @param connection The {@link SessionDescription#connection}.
* @return This builder.
*/
public Builder setConnection(String connection) {
this.connection = connection;
return this;
}
/**
* Sets {@link SessionDescription#bitrate}. The default is {@link Format#NO_VALUE}.
*
* @param bitrate The {@link SessionDescription#bitrate} in bits per second.
* @return This builder.
*/
public Builder setBitrate(int bitrate) {
this.bitrate = bitrate;
return this;
}
/**
* Sets {@link SessionDescription#timing}.
*
* <p>This property must be set before calling {@link #build()}.
*
* @param timing The {@link SessionDescription#timing}.
* @return This builder.
*/
public Builder setTiming(String timing) {
this.timing = timing;
return this;
}
/**
* Sets {@link SessionDescription#key}. The default is {@code null}.
*
* @param key The {@link SessionDescription#key}.
* @return This builder.
*/
public Builder setKey(String key) {
this.key = key;
return this;
}
/**
* Sets {@link SessionDescription#emailAddress}. The default is {@code null}.
*
* @param emailAddress The {@link SessionDescription#emailAddress}.
* @return This builder.
*/
public Builder setEmailAddress(String emailAddress) {
this.emailAddress = emailAddress;
return this;
}
/**
* Sets {@link SessionDescription#phoneNumber}. The default is {@code null}.
*
* @param phoneNumber The {@link SessionDescription#phoneNumber}.
* @return This builder.
*/
public Builder setPhoneNumber(String phoneNumber) {
this.phoneNumber = phoneNumber;
return this;
}
/**
* Adds one attribute to {@link SessionDescription#attributes}.
*
* @param attributeName The name of the attribute.
* @param attributeValue The value of the attribute.
* @return This builder.
*/
public Builder addAttribute(String attributeName, String attributeValue) {
attributesBuilder.put(attributeName, attributeValue);
return this;
}
/**
* Adds one {@link MediaDescription} to the {@link SessionDescription#mediaDescriptionList}.
*
* @param mediaDescription The {@link MediaDescription}.
* @return This builder.
*/
public Builder addMediaDescription(MediaDescription mediaDescription) {
mediaDescriptionListBuilder.add(mediaDescription);
return this;
}
/**
* Builds a new {@link SessionDescription} instance.
*
* @return The newly built {@link SessionDescription} instance.
* @throws IllegalStateException When one or more of {@link #sessionName}, {@link #timing} and
* {@link #origin} is not set.
*/
public SessionDescription build() {
if (sessionName == null || origin == null || timing == null) {
throw new IllegalStateException("One of more mandatory SDP fields are not set.");
}
return new SessionDescription(this);
}
}
/** The only supported SDP version, will be checked against every SDP message received. */
public static final String SUPPORTED_SDP_VERSION = "0";
/** The control attribute name. */
public static final String ATTR_CONTROL = "control";
/** The format property attribute name. */
public static final String ATTR_FMTP = "fmtp";
/** The length property attribute name. */
public static final String ATTR_LENGTH = "length";
/** The range property attribute name. */
public static final String ATTR_RANGE = "range";
/** The RTP format mapping property attribute name. */
public static final String ATTR_RTPMAP = "rtpmap";
/** The tool property attribute name. */
public static final String ATTR_TOOL = "tool";
/** The type property attribute name. */
public static final String ATTR_TYPE = "type";
/**
* All the session attributes, mapped from attribute name to value. The value is {@code ""} if not
* present.
*/
public final ImmutableMap<String, String> attributes;
/**
* The {@link MediaDescription MediaDescriptions} for each media track included in the session.
*/
public final ImmutableList<MediaDescription> mediaDescriptionList;
/** The name of a session. */
public final String sessionName;
// TODO(internal b/172331505) Parse the String representations into objects.
/** The origin sender info. */
public final String origin;
/** The timing info. */
public final String timing;
/** The estimated bitrate in bits per seconds. */
public final int bitrate;
/** The uri of a linked content. */
@Nullable public final Uri uri;
/** The connection info. */
@Nullable public final String connection;
/** The encryption method and key info. */
@Nullable public final String key;
/** The email info. */
@Nullable public final String emailAddress;
/** The phone number info. */
@Nullable public final String phoneNumber;
/** The session info, a detailed description of the session. */
@Nullable public final String sessionInfo;
/** Creates a new instance. */
private SessionDescription(Builder builder) {
this.attributes = builder.attributesBuilder.build();
this.mediaDescriptionList = builder.mediaDescriptionListBuilder.build();
this.sessionName = castNonNull(builder.sessionName);
this.origin = castNonNull(builder.origin);
this.timing = castNonNull(builder.timing);
this.uri = builder.uri;
this.connection = builder.connection;
this.bitrate = builder.bitrate;
this.key = builder.key;
this.emailAddress = builder.emailAddress;
this.phoneNumber = builder.phoneNumber;
this.sessionInfo = builder.sessionInfo;
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
SessionDescription that = (SessionDescription) o;
return bitrate == that.bitrate
&& attributes.equals(that.attributes)
&& mediaDescriptionList.equals(that.mediaDescriptionList)
&& origin.equals(that.origin)
&& sessionName.equals(that.sessionName)
&& timing.equals(that.timing)
&& Util.areEqual(sessionInfo, that.sessionInfo)
&& Util.areEqual(uri, that.uri)
&& Util.areEqual(emailAddress, that.emailAddress)
&& Util.areEqual(phoneNumber, that.phoneNumber)
&& Util.areEqual(connection, that.connection)
&& Util.areEqual(key, that.key);
}
@Override
public int hashCode() {
int result = 7;
result = 31 * result + attributes.hashCode();
result = 31 * result + mediaDescriptionList.hashCode();
result = 31 * result + origin.hashCode();
result = 31 * result + sessionName.hashCode();
result = 31 * result + timing.hashCode();
result = 31 * result + bitrate;
result = 31 * result + (sessionInfo == null ? 0 : sessionInfo.hashCode());
result = 31 * result + (uri == null ? 0 : uri.hashCode());
result = 31 * result + (emailAddress == null ? 0 : emailAddress.hashCode());
result = 31 * result + (phoneNumber == null ? 0 : phoneNumber.hashCode());
result = 31 * result + (connection == null ? 0 : connection.hashCode());
result = 31 * result + (key == null ? 0 : key.hashCode());
return result;
}
}

View File

@ -0,0 +1,231 @@
/*
* 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 com.google.android.exoplayer2.source.rtsp.sdp;
import static com.google.android.exoplayer2.source.rtsp.sdp.SessionDescription.SUPPORTED_SDP_VERSION;
import static com.google.android.exoplayer2.util.Assertions.checkArgument;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.common.base.Strings.nullToEmpty;
import android.net.Uri;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.util.Util;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** Parses a String based SDP message into {@link SessionDescription}. */
public final class SessionDescriptionParser {
// SDP line always starts with an one letter tag, followed by an equal sign. The information
// under the given tag follows an optional space.
private static final Pattern SDP_LINE_PATTERN = Pattern.compile("([a-z])=\\s?(.+)");
// Matches an attribute line (with a= sdp tag removed. Example: range:npt=0-50.0).
// Attribute can also be a flag, i.e. without a value, like recvonly.
private static final Pattern ATTRIBUTE_PATTERN = Pattern.compile("([0-9A-Za-z-]+)(?::(.*))?");
// SDP media description line: <mediaType> <port> <transmissionProtocol> <rtpPayloadType>
// For instance: audio 0 RTP/AVP 97
private static final Pattern MEDIA_DESCRIPTION_PATTERN =
Pattern.compile("(\\S+)\\s(\\S+)\\s(\\S+)\\s(\\S+)");
private static final String CRLF = "\r\n";
private static final String VERSION_TYPE = "v";
private static final String ORIGIN_TYPE = "o";
private static final String SESSION_TYPE = "s";
private static final String INFORMATION_TYPE = "i";
private static final String URI_TYPE = "u";
private static final String EMAIL_TYPE = "e";
private static final String PHONE_NUMBER_TYPE = "p";
private static final String CONNECTION_TYPE = "c";
private static final String BANDWIDTH_TYPE = "b";
private static final String TIMING_TYPE = "t";
private static final String KEY_TYPE = "k";
private static final String ATTRIBUTE_TYPE = "a";
private static final String MEDIA_TYPE = "m";
private static final String REPEAT_TYPE = "r";
private static final String ZONE_TYPE = "z";
/**
* Parses a String based SDP message into {@link SessionDescription}.
*
* @throws ParserException On SDP message line that cannot be parsed, or when one or more of the
* mandatory SDP fields {@link SessionDescription#timing}, {@link SessionDescription#origin}
* and {@link SessionDescription#sessionName} are not set.
*/
public static SessionDescription parse(String sdpString) throws ParserException {
SessionDescription.Builder sessionDescriptionBuilder = new SessionDescription.Builder();
@Nullable MediaDescription.Builder mediaDescriptionBuilder = null;
// Lines are separated by an CRLF.
for (String line : Util.split(sdpString, CRLF)) {
if ("".equals(line)) {
continue;
}
Matcher matcher = SDP_LINE_PATTERN.matcher(line);
if (!matcher.matches()) {
throw new ParserException("Malformed SDP line: " + line);
}
String sdpType = checkNotNull(matcher.group(1));
String sdpValue = checkNotNull(matcher.group(2));
switch (sdpType) {
case VERSION_TYPE:
if (!SUPPORTED_SDP_VERSION.equals(sdpValue)) {
throw new ParserException(String.format("SDP version %s is not supported.", sdpValue));
}
break;
case ORIGIN_TYPE:
sessionDescriptionBuilder.setOrigin(sdpValue);
break;
case SESSION_TYPE:
sessionDescriptionBuilder.setSessionName(sdpValue);
break;
case INFORMATION_TYPE:
if (mediaDescriptionBuilder == null) {
sessionDescriptionBuilder.setSessionInfo(sdpValue);
} else {
mediaDescriptionBuilder.setMediaTitle(sdpValue);
}
break;
case URI_TYPE:
sessionDescriptionBuilder.setUri(Uri.parse(sdpValue));
break;
case EMAIL_TYPE:
sessionDescriptionBuilder.setEmailAddress(sdpValue);
break;
case PHONE_NUMBER_TYPE:
sessionDescriptionBuilder.setPhoneNumber(sdpValue);
break;
case CONNECTION_TYPE:
if (mediaDescriptionBuilder == null) {
sessionDescriptionBuilder.setConnection(sdpValue);
} else {
mediaDescriptionBuilder.setConnection(sdpValue);
}
break;
case BANDWIDTH_TYPE:
String[] bandwidthComponents = Util.split(sdpValue, ":\\s?");
checkArgument(bandwidthComponents.length == 2);
int bitrateKbps = Integer.parseInt(bandwidthComponents[1]);
// Converting kilobits per second to bits per second.
if (mediaDescriptionBuilder == null) {
sessionDescriptionBuilder.setBitrate(bitrateKbps * 1000);
} else {
mediaDescriptionBuilder.setBitrate(bitrateKbps * 1000);
}
break;
case TIMING_TYPE:
sessionDescriptionBuilder.setTiming(sdpValue);
break;
case KEY_TYPE:
if (mediaDescriptionBuilder == null) {
sessionDescriptionBuilder.setKey(sdpValue);
} else {
mediaDescriptionBuilder.setKey(sdpValue);
}
break;
case ATTRIBUTE_TYPE:
matcher = ATTRIBUTE_PATTERN.matcher(sdpValue);
if (!matcher.matches()) {
throw new ParserException("Malformed Attribute line: " + line);
}
String attributeName = checkNotNull(matcher.group(1));
// The second catching group is optional and thus could be null.
String attributeValue = nullToEmpty(matcher.group(2));
if (mediaDescriptionBuilder == null) {
sessionDescriptionBuilder.addAttribute(attributeName, attributeValue);
} else {
mediaDescriptionBuilder.addAttribute(attributeName, attributeValue);
}
break;
case MEDIA_TYPE:
if (mediaDescriptionBuilder != null) {
addMediaDescriptionToSession(sessionDescriptionBuilder, mediaDescriptionBuilder);
}
mediaDescriptionBuilder = parseMediaDescriptionLine(sdpValue);
break;
case REPEAT_TYPE:
case ZONE_TYPE:
default:
// Not handled.
}
}
if (mediaDescriptionBuilder != null) {
addMediaDescriptionToSession(sessionDescriptionBuilder, mediaDescriptionBuilder);
}
try {
return sessionDescriptionBuilder.build();
} catch (IllegalStateException e) {
throw new ParserException(e);
}
}
private static void addMediaDescriptionToSession(
SessionDescription.Builder sessionDescriptionBuilder,
MediaDescription.Builder mediaDescriptionBuilder)
throws ParserException {
try {
sessionDescriptionBuilder.addMediaDescription(mediaDescriptionBuilder.build());
} catch (IllegalStateException e) {
throw new ParserException(e);
}
}
private static MediaDescription.Builder parseMediaDescriptionLine(String line)
throws ParserException {
Matcher matcher = MEDIA_DESCRIPTION_PATTERN.matcher(line);
if (!matcher.matches()) {
throw new ParserException("Malformed SDP media description line: " + line);
}
String mediaType = checkNotNull(matcher.group(1));
String portString = checkNotNull(matcher.group(2));
String transportProtocol = checkNotNull(matcher.group(3));
String payloadTypeString = checkNotNull(matcher.group(4));
try {
return new MediaDescription.Builder(
mediaType,
Integer.parseInt(portString),
transportProtocol,
Integer.parseInt(payloadTypeString));
} catch (NumberFormatException e) {
throw new ParserException("Malformed SDP media description line: " + line, e);
}
}
/** Prevents initialization. */
private SessionDescriptionParser() {}
}

View File

@ -0,0 +1,19 @@
/*
* 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.
*/
@NonNullApi
package com.google.android.exoplayer2.source.rtsp.sdp;
import com.google.android.exoplayer2.util.NonNullApi;

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2020 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.
-->
<manifest package="com.google.android.exoplayer2.source.rtsp.test">
<uses-sdk/>
</manifest>

View File

@ -0,0 +1,78 @@
/*
* 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 com.google.android.exoplayer2.source.rtsp;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import android.net.Uri;
import androidx.annotation.Nullable;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.robolectric.RobolectricUtil;
import com.google.android.exoplayer2.source.rtsp.RtspClient.SessionInfoListener;
import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableList;
import java.util.concurrent.atomic.AtomicBoolean;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Tests the {@link RtspClient} using the {@link RtspServer}. */
@RunWith(AndroidJUnit4.class)
public final class RtspClientTest {
private @MonotonicNonNull RtspClient rtspClient;
private @MonotonicNonNull RtspServer rtspServer;
@Before
public void setUp() {
rtspServer = new RtspServer();
}
@After
public void tearDown() {
Util.closeQuietly(rtspServer);
Util.closeQuietly(rtspClient);
}
@Test
public void connectServerAndClient_withServerSupportsOnlyOptions_sessionTimelineRequestFails()
throws Exception {
int serverRtspPortNumber = checkNotNull(rtspServer).startAndGetPortNumber();
AtomicBoolean sessionTimelineUpdateEventReceived = new AtomicBoolean();
rtspClient =
new RtspClient(
new SessionInfoListener() {
@Override
public void onSessionTimelineUpdated(
RtspSessionTiming timing, ImmutableList<RtspMediaTrack> tracks) {}
@Override
public void onSessionTimelineRequestFailed(
String message, @Nullable Throwable cause) {
sessionTimelineUpdateEventReceived.set(true);
}
},
/* userAgent= */ "ExoPlayer:RtspClientTest",
/* uri= */ Uri.parse(
Util.formatInvariant("rtsp://localhost:%d/test", serverRtspPortNumber)));
rtspClient.start();
RobolectricUtil.runMainLooperUntil(sessionTimelineUpdateEventReceived::get);
}
}

View File

@ -0,0 +1,102 @@
/*
* 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 com.google.android.exoplayer2.source.rtsp;
import static com.google.android.exoplayer2.robolectric.RobolectricUtil.runMainLooperUntil;
import static com.google.common.truth.Truth.assertThat;
import android.net.Uri;
import androidx.annotation.Nullable;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.rtsp.sdp.MediaDescription;
import com.google.android.exoplayer2.source.rtsp.sdp.SessionDescription;
import com.google.android.exoplayer2.upstream.DefaultAllocator;
import com.google.common.collect.ImmutableList;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit test for {@link RtspMediaPeriod}. */
@RunWith(AndroidJUnit4.class)
public class RtspMediaPeriodTest {
private static final RtspClient PLACEHOLDER_RTSP_CLIENT =
new RtspClient(
new RtspClient.SessionInfoListener() {
@Override
public void onSessionTimelineUpdated(
RtspSessionTiming timing, ImmutableList<RtspMediaTrack> tracks) {}
@Override
public void onSessionTimelineRequestFailed(String message, @Nullable Throwable cause) {}
},
/* userAgent= */ null,
Uri.EMPTY);
@Test
public void prepare_startsLoading() throws Exception {
RtspMediaPeriod rtspMediaPeriod =
new RtspMediaPeriod(
new DefaultAllocator(/* trimOnReset= */ true, C.DEFAULT_BUFFER_SEGMENT_SIZE),
ImmutableList.of(
new RtspMediaTrack(
new MediaDescription.Builder(
/* mediaType= */ MediaDescription.MEDIA_TYPE_VIDEO,
/* port= */ 0,
/* transportProtocol= */ MediaDescription.RTP_AVP_PROFILE,
/* payloadType= */ 96)
.setConnection("IN IP4 0.0.0.0")
.setBitrate(500_000)
.addAttribute(SessionDescription.ATTR_RTPMAP, "96 H264/90000")
.addAttribute(
SessionDescription.ATTR_FMTP,
"96 packetization-mode=1;profile-level-id=64001F;sprop-parameter-sets=Z2QAH6zZQPARabIAAAMACAAAAwGcHjBjLA==,aOvjyyLA")
.addAttribute(SessionDescription.ATTR_CONTROL, "track1")
.build(),
Uri.parse("rtsp://localhost/test"))),
PLACEHOLDER_RTSP_CLIENT);
AtomicBoolean prepareCallbackCalled = new AtomicBoolean(false);
rtspMediaPeriod.prepare(
new MediaPeriod.Callback() {
@Override
public void onPrepared(MediaPeriod mediaPeriod) {
prepareCallbackCalled.set(true);
}
@Override
public void onContinueLoadingRequested(MediaPeriod source) {
source.continueLoading(/* positionUs= */ 0);
}
},
/* positionUs= */ 0);
runMainLooperUntil(prepareCallbackCalled::get);
rtspMediaPeriod.release();
}
@Test
public void getBufferedPositionUs_withNoRtspMediaTracks_returnsEndOfSource() {
RtspMediaPeriod rtspMediaPeriod =
new RtspMediaPeriod(
new DefaultAllocator(/* trimOnReset= */ true, C.DEFAULT_BUFFER_SEGMENT_SIZE),
ImmutableList.of(),
PLACEHOLDER_RTSP_CLIENT);
assertThat(rtspMediaPeriod.getBufferedPositionUs()).isEqualTo(C.TIME_END_OF_SOURCE);
}
}

View File

@ -0,0 +1,260 @@
/*
* 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 com.google.android.exoplayer2.source.rtsp;
import static com.google.android.exoplayer2.source.rtsp.sdp.MediaDescription.MEDIA_TYPE_AUDIO;
import static com.google.android.exoplayer2.source.rtsp.sdp.MediaDescription.MEDIA_TYPE_VIDEO;
import static com.google.android.exoplayer2.source.rtsp.sdp.MediaDescription.RTP_AVP_PROFILE;
import static com.google.android.exoplayer2.source.rtsp.sdp.SessionDescription.ATTR_CONTROL;
import static com.google.android.exoplayer2.source.rtsp.sdp.SessionDescription.ATTR_FMTP;
import static com.google.android.exoplayer2.source.rtsp.sdp.SessionDescription.ATTR_RTPMAP;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.audio.AacUtil;
import com.google.android.exoplayer2.source.rtsp.rtp.RtpPayloadFormat;
import com.google.android.exoplayer2.source.rtsp.sdp.MediaDescription;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit test for {@link RtspMediaTrack}. */
@RunWith(AndroidJUnit4.class)
public class RtspMediaTrackTest {
@Test
public void generatePayloadFormat_withH264MediaDescription_succeeds() throws Exception {
MediaDescription mediaDescription =
new MediaDescription.Builder(MEDIA_TYPE_VIDEO, 0, RTP_AVP_PROFILE, 96)
.setConnection("IN IP4 0.0.0.0")
.setBitrate(500_000)
.addAttribute(ATTR_RTPMAP, "96 H264/90000")
.addAttribute(
ATTR_FMTP,
"96 packetization-mode=1;profile-level-id=64001F;sprop-parameter-sets=Z2QAH6zZQPARabIAAAMACAAAAwGcHjBjLA==,aOvjyyLA")
.addAttribute(ATTR_CONTROL, "track1")
.build();
RtpPayloadFormat format = RtspMediaTrack.generatePayloadFormat(mediaDescription);
RtpPayloadFormat expectedFormat =
new RtpPayloadFormat(
new Format.Builder()
.setSampleMimeType(MimeTypes.VIDEO_H264)
.setAverageBitrate(500_000)
.setPixelWidthHeightRatio(1.0f)
.setHeight(544)
.setWidth(960)
.setCodecs("avc1.64001F")
.setInitializationData(
ImmutableList.of(
new byte[] {
0, 0, 0, 1, 103, 100, 0, 31, -84, -39, 64, -16, 17, 105, -78, 0, 0, 3, 0,
8, 0, 0, 3, 1, -100, 30, 48, 99, 44
},
new byte[] {0, 0, 0, 1, 104, -21, -29, -53, 34, -64}))
.build(),
/* rtpPayloadType= */ 96,
/* clockRate= */ 90_000,
/* fmtpParameters= */ ImmutableMap.of(
"packetization-mode", "1",
"profile-level-id", "64001F",
"sprop-parameter-sets", "Z2QAH6zZQPARabIAAAMACAAAAwGcHjBjLA==,aOvjyyLA"));
assertThat(format).isEqualTo(expectedFormat);
}
@Test
public void generatePayloadFormat_withAacMediaDescription_succeeds() throws Exception {
MediaDescription mediaDescription =
new MediaDescription.Builder(MEDIA_TYPE_AUDIO, 0, RTP_AVP_PROFILE, 97)
.setConnection("IN IP4 0.0.0.0")
.setBitrate(96_000)
.addAttribute(ATTR_RTPMAP, "97 MPEG4-GENERIC/44100")
.addAttribute(
ATTR_FMTP,
"97 streamtype=5; profile-level-id=1; mode=AAC-hbr; sizelength=13; indexlength=3;"
+ " indexdeltalength=3; config=1208")
.addAttribute(ATTR_CONTROL, "track2")
.build();
RtpPayloadFormat format = RtspMediaTrack.generatePayloadFormat(mediaDescription);
RtpPayloadFormat expectedFormat =
new RtpPayloadFormat(
new Format.Builder()
.setSampleMimeType(MimeTypes.AUDIO_AAC)
.setChannelCount(1)
.setSampleRate(44100)
.setAverageBitrate(96_000)
.setCodecs("mp4a.40.1")
.setInitializationData(
ImmutableList.of(
AacUtil.buildAacLcAudioSpecificConfig(
/* sampleRate= */ 44_100, /* channelCount= */ 1)))
.build(),
/* rtpPayloadType= */ 97,
/* clockRate= */ 44_100,
/* fmtpParameters= */ new ImmutableMap.Builder<String, String>()
.put("streamtype", "5")
.put("profile-level-id", "1")
.put("mode", "AAC-hbr")
.put("sizelength", "13")
.put("indexlength", "3")
.put("indexdeltalength", "3")
.put("config", "1208")
.build());
assertThat(format).isEqualTo(expectedFormat);
}
@Test
public void generatePayloadFormat_withAc3MediaDescriptionWithDefaultChannelCount_succeeds()
throws Exception {
MediaDescription mediaDescription =
new MediaDescription.Builder(MEDIA_TYPE_AUDIO, 0, RTP_AVP_PROFILE, 97)
.setConnection("IN IP4 0.0.0.0")
.setBitrate(48_000)
.addAttribute(ATTR_RTPMAP, "97 AC3/48000")
.addAttribute(ATTR_CONTROL, "track2")
.build();
RtpPayloadFormat format = RtspMediaTrack.generatePayloadFormat(mediaDescription);
RtpPayloadFormat expectedFormat =
new RtpPayloadFormat(
new Format.Builder()
.setSampleMimeType(MimeTypes.AUDIO_AC3)
.setChannelCount(6)
.setSampleRate(48000)
.setAverageBitrate(48_000)
.build(),
/* rtpPayloadType= */ 97,
/* clockRate= */ 48000,
/* fmtpParameters= */ ImmutableMap.of());
assertThat(format).isEqualTo(expectedFormat);
}
@Test
public void generatePayloadFormat_withAc3MediaDescription_succeeds() throws Exception {
MediaDescription mediaDescription =
new MediaDescription.Builder(MEDIA_TYPE_AUDIO, 0, RTP_AVP_PROFILE, 97)
.setConnection("IN IP4 0.0.0.0")
.setBitrate(48_000)
.addAttribute(ATTR_RTPMAP, "97 AC3/48000/2")
.addAttribute(ATTR_CONTROL, "track2")
.build();
RtpPayloadFormat format = RtspMediaTrack.generatePayloadFormat(mediaDescription);
RtpPayloadFormat expectedFormat =
new RtpPayloadFormat(
new Format.Builder()
.setSampleMimeType(MimeTypes.AUDIO_AC3)
.setChannelCount(2)
.setSampleRate(48000)
.setAverageBitrate(48_000)
.build(),
/* rtpPayloadType= */ 97,
/* clockRate= */ 48000,
/* fmtpParameters= */ ImmutableMap.of());
assertThat(format).isEqualTo(expectedFormat);
}
@Test
public void
generatePayloadFormat_withAacMediaDescriptionMissingFmtpAttribute_throwsIllegalArgumentException() {
MediaDescription mediaDescription =
new MediaDescription.Builder(MEDIA_TYPE_AUDIO, 0, RTP_AVP_PROFILE, 97)
.setConnection("IN IP4 0.0.0.0")
.setBitrate(96_000)
.addAttribute(ATTR_RTPMAP, "97 MPEG4-GENERIC/44100")
.addAttribute(ATTR_CONTROL, "track2")
.build();
assertThrows(
IllegalArgumentException.class,
() -> RtspMediaTrack.generatePayloadFormat(mediaDescription));
}
@Test
public void
generatePayloadFormat_withMediaDescriptionMissingProfileLevel_throwsIllegalArgumentException() {
MediaDescription mediaDescription =
new MediaDescription.Builder(MEDIA_TYPE_AUDIO, 0, RTP_AVP_PROFILE, 97)
.setConnection("IN IP4 0.0.0.0")
.setBitrate(96_000)
.addAttribute(ATTR_RTPMAP, "97 MPEG4-GENERIC/44100")
.addAttribute(
ATTR_FMTP,
"97 streamtype=5;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1208")
.addAttribute(ATTR_CONTROL, "track2")
.build();
assertThrows(
IllegalArgumentException.class,
() -> RtspMediaTrack.generatePayloadFormat(mediaDescription));
}
@Test
public void
generatePayloadFormat_withH264MediaDescriptionMissingFmtpAttribute_throwsIllegalArgumentException() {
MediaDescription mediaDescription =
new MediaDescription.Builder(MEDIA_TYPE_VIDEO, 0, RTP_AVP_PROFILE, 96)
.setConnection("IN IP4 0.0.0.0")
.setBitrate(500_000)
.addAttribute(ATTR_RTPMAP, "96 H264/90000")
.addAttribute(ATTR_CONTROL, "track1")
.build();
assertThrows(
IllegalArgumentException.class,
() -> RtspMediaTrack.generatePayloadFormat(mediaDescription));
}
@Test
public void
generatePayloadFormat_withH264MediaDescriptionMissingProfileLevel_throwsIllegalArgumentException() {
MediaDescription mediaDescription =
new MediaDescription.Builder(MEDIA_TYPE_VIDEO, 0, RTP_AVP_PROFILE, 96)
.setConnection("IN IP4 0.0.0.0")
.setBitrate(500_000)
.addAttribute(ATTR_RTPMAP, "96 H264/90000")
.addAttribute(
ATTR_FMTP,
"96 packetization-mode=1;sprop-parameter-sets=Z2QAH6zZQPARabIAAAMACAAAAwGcHjBjLA==,aOvjyyLA")
.addAttribute(ATTR_CONTROL, "track1")
.build();
assertThrows(
IllegalArgumentException.class,
() -> RtspMediaTrack.generatePayloadFormat(mediaDescription));
}
@Test
public void
generatePayloadFormat_withH264MediaDescriptionMissingSpropParameter_throwsIllegalArgumentException() {
MediaDescription mediaDescription =
new MediaDescription.Builder(MEDIA_TYPE_VIDEO, 0, RTP_AVP_PROFILE, 96)
.setConnection("IN IP4 0.0.0.0")
.setBitrate(500_000)
.addAttribute(ATTR_RTPMAP, "96 H264/90000")
.addAttribute(ATTR_FMTP, "96 packetization-mode=1;profile-level-id=64001F")
.addAttribute(ATTR_CONTROL, "track1")
.build();
assertThrows(
IllegalArgumentException.class,
() -> RtspMediaTrack.generatePayloadFormat(mediaDescription));
}
}

View File

@ -0,0 +1,155 @@
/*
* 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 com.google.android.exoplayer2.source.rtsp;
import static com.google.android.exoplayer2.source.rtsp.message.RtspRequest.METHOD_OPTIONS;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import android.os.Handler;
import android.os.Looper;
import com.google.android.exoplayer2.source.rtsp.message.RtspHeaders;
import com.google.android.exoplayer2.source.rtsp.message.RtspMessageChannel;
import com.google.android.exoplayer2.source.rtsp.message.RtspMessageUtil;
import com.google.android.exoplayer2.source.rtsp.message.RtspRequest;
import com.google.android.exoplayer2.source.rtsp.message.RtspResponse;
import com.google.android.exoplayer2.util.Util;
import java.io.Closeable;
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.util.List;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** The RTSP server. */
public final class RtspServer implements Closeable {
private static final String PUBLIC_SUPPORTED_METHODS = "OPTIONS";
/** RTSP error Method Not Allowed (RFC2326 Section 7.1.1). */
private static final int STATUS_METHOD_NOT_ALLOWED = 405;
private final Thread listenerThread;
/** Runs on the thread on which the constructor was called. */
private final Handler mainHandler;
private @MonotonicNonNull ServerSocket serverSocket;
private @MonotonicNonNull RtspMessageChannel connectedClient;
private volatile boolean isCanceled;
/**
* Creates a new instance.
*
* <p>The constructor must be called on a {@link Looper} thread.
*/
public RtspServer() {
listenerThread =
new Thread(this::listenToIncomingRtspConnection, "ExoPlayerTest:RtspConnectionMonitor");
mainHandler = Util.createHandlerForCurrentLooper();
}
/**
* Starts the server. The server starts listening to incoming RTSP connections.
*
* <p>The user must call {@link #close} if {@link IOException} is thrown. Closed instances must
* not be started again.
*
* @return The server side port number for RTSP connections.
*/
public int startAndGetPortNumber() throws IOException {
// Auto assign port and allow only one client connection (backlog).
serverSocket =
new ServerSocket(/* port= */ 0, /* backlog= */ 1, InetAddress.getByName(/* host= */ null));
listenerThread.start();
return serverSocket.getLocalPort();
}
@Override
public void close() throws IOException {
isCanceled = true;
if (serverSocket != null) {
serverSocket.close();
}
if (connectedClient != null) {
connectedClient.close();
}
}
private void handleNewClientConnected(Socket socket) {
try {
connectedClient = new RtspMessageChannel(new MessageListener());
connectedClient.openSocket(socket);
} catch (IOException e) {
Util.closeQuietly(connectedClient);
// Log the error.
e.printStackTrace();
}
}
private final class MessageListener implements RtspMessageChannel.MessageListener {
@Override
public void onRtspMessageReceived(List<String> message) {
RtspRequest request = RtspMessageUtil.parseRequest(message);
switch (request.method) {
case METHOD_OPTIONS:
onOptionsRequestReceived(request);
break;
default:
connectedClient.send(
RtspMessageUtil.serializeResponse(
new RtspResponse(
/* status= */ STATUS_METHOD_NOT_ALLOWED,
/* headers= */ new RtspHeaders.Builder()
.add(
RtspHeaders.CSEQ, checkNotNull(request.headers.get(RtspHeaders.CSEQ)))
.build(),
/* messageBody= */ "")));
}
}
private void onOptionsRequestReceived(RtspRequest request) {
connectedClient.send(
RtspMessageUtil.serializeResponse(
new RtspResponse(
/* status= */ 200,
/* headers= */ new RtspHeaders.Builder()
.add(RtspHeaders.CSEQ, checkNotNull(request.headers.get(RtspHeaders.CSEQ)))
.add(RtspHeaders.PUBLIC, PUBLIC_SUPPORTED_METHODS)
.build(),
/* messageBody= */ "")));
}
}
private void listenToIncomingRtspConnection() {
while (!isCanceled) {
try {
Socket acceptedClientSocket = serverSocket.accept();
mainHandler.post(() -> handleNewClientConnected(acceptedClientSocket));
} catch (SocketException e) {
// SocketException is thrown when serverSocket is closed while running accept().
if (!isCanceled) {
isCanceled = true;
e.printStackTrace();
}
} catch (IOException e) {
isCanceled = true;
// Log the error.
e.printStackTrace();
}
}
}
}

View File

@ -0,0 +1,62 @@
/*
* 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 com.google.android.exoplayer2.source.rtsp;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit test for {@link RtspSessionTiming}. */
@RunWith(AndroidJUnit4.class)
public class RtspSessionTimingTest {
@Test
public void parseTiming_withNowLiveTiming() throws Exception {
RtspSessionTiming sessionTiming = RtspSessionTiming.parseTiming("npt=now-");
assertThat(sessionTiming.getDurationMs()).isEqualTo(C.TIME_UNSET);
assertThat(sessionTiming.isLive()).isTrue();
}
@Test
public void parseTiming_withZeroLiveTiming() throws Exception {
RtspSessionTiming sessionTiming = RtspSessionTiming.parseTiming("npt=0-");
assertThat(sessionTiming.getDurationMs()).isEqualTo(C.TIME_UNSET);
assertThat(sessionTiming.isLive()).isTrue();
}
@Test
public void parseTiming_withDecimalZeroLiveTiming() throws Exception {
RtspSessionTiming sessionTiming = RtspSessionTiming.parseTiming("npt=0.000-");
assertThat(sessionTiming.getDurationMs()).isEqualTo(C.TIME_UNSET);
assertThat(sessionTiming.isLive()).isTrue();
}
@Test
public void parseTiming_withRangeTiming() throws Exception {
RtspSessionTiming sessionTiming = RtspSessionTiming.parseTiming("npt=0.000-32.054");
assertThat(sessionTiming.getDurationMs()).isEqualTo(32054);
assertThat(sessionTiming.isLive()).isFalse();
}
@Test
public void parseTiming_withInvalidRangeTiming_throwsIllegalArgumentException() {
assertThrows(
IllegalArgumentException.class, () -> RtspSessionTiming.parseTiming("npt=10.000-2.054"));
}
}

View File

@ -0,0 +1,93 @@
/*
* 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 com.google.android.exoplayer2.source.rtsp;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import android.net.Uri;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException;
import com.google.common.collect.ImmutableList;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit test for {@link RtspTrackTiming}. */
@RunWith(AndroidJUnit4.class)
public class RtspTrackTimingTest {
@Test
public void parseTiming_withSeqNumberAndRtpTime() throws Exception {
String rtpInfoString =
"url=rtsp://video.example.com/twister/video;seq=12312232;rtptime=78712811";
ImmutableList<RtspTrackTiming> trackTimingList =
RtspTrackTiming.parseTrackTiming(rtpInfoString);
assertThat(trackTimingList).hasSize(1);
RtspTrackTiming trackTiming = trackTimingList.get(0);
assertThat(trackTiming.uri).isEqualTo(Uri.parse("rtsp://video.example.com/twister/video"));
assertThat(trackTiming.sequenceNumber).isEqualTo(12312232);
assertThat(trackTiming.rtpTimestamp).isEqualTo(78712811);
}
@Test
public void parseTiming_withSeqNumberOnly() throws Exception {
String rtpInfoString =
"url=rtsp://foo.com/bar.avi/streamid=0;seq=45102,url=rtsp://foo.com/bar.avi/streamid=1;seq=30211";
ImmutableList<RtspTrackTiming> trackTimingList =
RtspTrackTiming.parseTrackTiming(rtpInfoString);
assertThat(trackTimingList).hasSize(2);
RtspTrackTiming trackTiming = trackTimingList.get(0);
assertThat(trackTiming.uri).isEqualTo(Uri.parse("rtsp://foo.com/bar.avi/streamid=0"));
assertThat(trackTiming.sequenceNumber).isEqualTo(45102);
assertThat(trackTiming.rtpTimestamp).isEqualTo(C.TIME_UNSET);
trackTiming = trackTimingList.get(1);
assertThat(trackTiming.uri).isEqualTo(Uri.parse("rtsp://foo.com/bar.avi/streamid=1"));
assertThat(trackTiming.sequenceNumber).isEqualTo(30211);
assertThat(trackTiming.rtpTimestamp).isEqualTo(C.TIME_UNSET);
}
@Test
public void parseTiming_withInvalidParameter_throws() {
String rtpInfoString = "url=rtsp://video.example.com/twister/video;seq=123abc";
assertThrows(ParserException.class, () -> RtspTrackTiming.parseTrackTiming(rtpInfoString));
}
@Test
public void parseTiming_withInvalidUrl_throws() {
String rtpInfoString = "url=video.example.com/twister/video;seq=36192348";
assertThrows(ParserException.class, () -> RtspTrackTiming.parseTrackTiming(rtpInfoString));
}
@Test
public void parseTiming_withNoParameter_throws() {
String rtpInfoString = "url=rtsp://video.example.com/twister/video";
assertThrows(ParserException.class, () -> RtspTrackTiming.parseTrackTiming(rtpInfoString));
}
@Test
public void parseTiming_withNoUrl_throws() {
String rtpInfoString = "seq=35421887";
assertThrows(ParserException.class, () -> RtspTrackTiming.parseTrackTiming(rtpInfoString));
}
}

View File

@ -0,0 +1,102 @@
/*
* 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 com.google.android.exoplayer2.source.rtsp.message;
import static com.google.common.truth.Truth.assertThat;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit test for {@link RtspHeaders}. */
@RunWith(AndroidJUnit4.class)
public final class RtspHeadersTest {
@Test
public void build_withHeaderLines() {
RtspHeaders headers =
new RtspHeaders.Builder()
.addAll(
ImmutableList.of(
"Accept: application/sdp ", // Extra space after header value.
"CSeq:3", // No space after colon.
"Content-Length: 707",
"Transport: RTP/AVP;unicast;client_port=65458-65459\r\n"))
.build();
assertThat(headers.get("Accept")).isEqualTo("application/sdp");
assertThat(headers.get("CSeq")).isEqualTo("3");
assertThat(headers.get("Content-Length")).isEqualTo("707");
assertThat(headers.get("Transport")).isEqualTo("RTP/AVP;unicast;client_port=65458-65459");
}
@Test
public void build_withHeaderLinesAsMap() {
RtspHeaders headers =
new RtspHeaders.Builder()
.addAll(
ImmutableMap.of(
"Accept", " application/sdp ", // Extra space after header value.
"CSeq", "3", // No space after colon.
"Content-Length", "707",
"Transport", "RTP/AVP;unicast;client_port=65458-65459\r\n"))
.build();
assertThat(headers.get("Accept")).isEqualTo("application/sdp");
assertThat(headers.get("CSeq")).isEqualTo("3");
assertThat(headers.get("Content-Length")).isEqualTo("707");
assertThat(headers.get("Transport")).isEqualTo("RTP/AVP;unicast;client_port=65458-65459");
}
@Test
public void get_getsHeaderValuesCaseInsensitively() {
RtspHeaders headers =
new RtspHeaders.Builder()
.addAll(
ImmutableList.of(
"ACCEPT: application/sdp ", // Extra space after header value.
"Cseq:3", // No space after colon.
"Content-LENGTH: 707",
"transport: RTP/AVP;unicast;client_port=65458-65459\r\n"))
.build();
assertThat(headers.get("Accept")).isEqualTo("application/sdp");
assertThat(headers.get("CSeq")).isEqualTo("3");
assertThat(headers.get("Content-Length")).isEqualTo("707");
assertThat(headers.get("Transport")).isEqualTo("RTP/AVP;unicast;client_port=65458-65459");
}
@Test
public void asMap() {
RtspHeaders headers =
new RtspHeaders.Builder()
.addAll(
ImmutableList.of(
"Accept: application/sdp ", // Extra space after header value.
"CSeq:3", // No space after colon.
"Content-Length: 707",
"Transport: RTP/AVP;unicast;client_port=65458-65459\r\n"))
.build();
assertThat(headers.asMap())
.containsExactly(
"Accept", "application/sdp",
"CSeq", "3",
"Content-Length", "707",
"Transport", "RTP/AVP;unicast;client_port=65458-65459");
}
}

View File

@ -0,0 +1,366 @@
/*
* 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 com.google.android.exoplayer2.source.rtsp.message;
import static com.google.common.truth.Truth.assertThat;
import android.net.Uri;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.base.Charsets;
import com.google.common.collect.ImmutableMap;
import java.util.Arrays;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit test for {@link RtspMessageUtil}. */
@RunWith(AndroidJUnit4.class)
public final class RtspMessageUtilTest {
@Test
public void parseRequest_withOptionsRequest_succeeds() {
List<String> requestLines =
Arrays.asList(
"OPTIONS rtsp://localhost:554/foo.bar RTSP/1.0",
"CSeq: 2",
"User-Agent: LibVLC/3.0.11",
"");
RtspRequest request = RtspMessageUtil.parseRequest(requestLines);
assertThat(request.method).isEqualTo(RtspRequest.METHOD_OPTIONS);
assertThat(request.headers.asMap())
.containsExactly(RtspHeaders.CSEQ, "2", RtspHeaders.USER_AGENT, "LibVLC/3.0.11");
assertThat(request.messageBody).isEmpty();
}
@Test
public void parseResponse_withOptionsResponse_succeeds() {
List<String> responseLines =
Arrays.asList(
"RTSP/1.0 200 OK",
"CSeq: 2",
"Public: OPTIONS, DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, GET_PARAMETER, SET_PARAMETER",
"");
RtspResponse response = RtspMessageUtil.parseResponse(responseLines);
assertThat(response.status).isEqualTo(200);
assertThat(response.headers.asMap())
.containsExactly(
RtspHeaders.CSEQ,
"2",
RtspHeaders.PUBLIC,
"OPTIONS, DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, GET_PARAMETER, SET_PARAMETER");
assertThat(response.messageBody).isEmpty();
}
@Test
public void parseRequest_withDescribeRequest_succeeds() {
List<String> requestLines =
Arrays.asList(
"DESCRIBE rtsp://localhost:554/foo.bar RTSP/1.0",
"CSeq: 3",
"User-Agent: LibVLC/3.0.11",
"Accept: application/sdp",
"");
RtspRequest request = RtspMessageUtil.parseRequest(requestLines);
assertThat(request.method).isEqualTo(RtspRequest.METHOD_DESCRIBE);
assertThat(request.headers.asMap())
.containsExactly(
RtspHeaders.CSEQ,
"3",
RtspHeaders.USER_AGENT,
"LibVLC/3.0.11",
RtspHeaders.ACCEPT,
"application/sdp");
assertThat(request.messageBody).isEmpty();
}
@Test
public void parseResponse_withDescribeResponse_succeeds() {
List<String> responseLines =
Arrays.asList(
"RTSP/1.0 200 OK",
"CSeq: 3",
"Content-Base: rtsp://127.0.0.1/test.mkv/",
"Content-Type: application/sdp",
"Content-Length: 707",
"",
"v=0",
"o=- 1606776316530225 1 IN IP4 192.168.2.176",
"i=imax.mkv",
"m=video 0 RTP/AVP 96",
"a=rtpmap:96 H264/90000",
"a=control:track1",
"m=audio 0 RTP/AVP 97",
"a=rtpmap:97 AC3/48000",
"a=control:track2");
RtspResponse response = RtspMessageUtil.parseResponse(responseLines);
assertThat(response.status).isEqualTo(200);
assertThat(response.headers.asMap())
.containsExactly(
RtspHeaders.CSEQ,
"3",
RtspHeaders.CONTENT_BASE,
"rtsp://127.0.0.1/test.mkv/",
RtspHeaders.CONTENT_TYPE,
"application/sdp",
RtspHeaders.CONTENT_LENGTH,
"707");
assertThat(response.messageBody)
.isEqualTo(
"v=0\r\n"
+ "o=- 1606776316530225 1 IN IP4 192.168.2.176\r\n"
+ "i=imax.mkv\r\n"
+ "m=video 0 RTP/AVP 96\r\n"
+ "a=rtpmap:96 H264/90000\r\n"
+ "a=control:track1\r\n"
+ "m=audio 0 RTP/AVP 97\r\n"
+ "a=rtpmap:97 AC3/48000\r\n"
+ "a=control:track2");
}
@Test
public void parseRequest_withSetParameterRequest_succeeds() {
List<String> requestLines =
Arrays.asList(
"SET_PARAMETER rtsp://localhost:554/foo.bar RTSP/1.0",
"CSeq: 3",
"User-Agent: LibVLC/3.0.11",
"Content-Length: 20",
"Content-Type: text/parameters",
"",
"param: stuff");
RtspRequest request = RtspMessageUtil.parseRequest(requestLines);
assertThat(request.method).isEqualTo(RtspRequest.METHOD_SET_PARAMETER);
assertThat(request.headers.asMap())
.containsExactly(
RtspHeaders.CSEQ,
"3",
RtspHeaders.USER_AGENT,
"LibVLC/3.0.11",
RtspHeaders.CONTENT_LENGTH,
"20",
RtspHeaders.CONTENT_TYPE,
"text/parameters");
assertThat(request.messageBody).isEqualTo("param: stuff");
}
@Test
public void parseResponse_withGetParameterResponse_succeeds() {
List<String> responseLines =
Arrays.asList(
"RTSP/1.0 200 OK",
"CSeq: 431",
"Content-Length: 46",
"Content-Type: text/parameters",
"",
"packets_received: 10",
"jitter: 0.3838");
RtspResponse response = RtspMessageUtil.parseResponse(responseLines);
assertThat(response.status).isEqualTo(200);
assertThat(response.headers.asMap())
.containsExactly(
RtspHeaders.CSEQ,
"431",
RtspHeaders.CONTENT_LENGTH,
"46",
RtspHeaders.CONTENT_TYPE,
"text/parameters");
assertThat(response.messageBody).isEqualTo("packets_received: 10\r\n" + "jitter: 0.3838");
}
@Test
public void serialize_setupRequest_succeeds() {
RtspRequest request =
new RtspRequest(
Uri.parse("rtsp://127.0.0.1/test.mkv/track1"),
RtspRequest.METHOD_SETUP,
new RtspHeaders.Builder()
.addAll(
ImmutableMap.of(
RtspHeaders.CSEQ, "4",
RtspHeaders.TRANSPORT, "RTP/AVP;unicast;client_port=65458-65459"))
.build(),
/* messageBody= */ "");
List<String> messageLines = RtspMessageUtil.serializeRequest(request);
List<String> expectedLines =
Arrays.asList(
"SETUP rtsp://127.0.0.1/test.mkv/track1 RTSP/1.0",
"CSeq: 4",
"Transport: RTP/AVP;unicast;client_port=65458-65459",
"",
"");
String expectedRtspMessage =
"SETUP rtsp://127.0.0.1/test.mkv/track1 RTSP/1.0\r\n"
+ "CSeq: 4\r\n"
+ "Transport: RTP/AVP;unicast;client_port=65458-65459\r\n"
+ "\r\n";
assertThat(messageLines).isEqualTo(expectedLines);
assertThat(RtspMessageUtil.convertMessageToByteArray(messageLines))
.isEqualTo(expectedRtspMessage.getBytes(Charsets.UTF_8));
}
@Test
public void serialize_setupResponse_succeeds() {
RtspResponse response =
new RtspResponse(
/* status= */ 200,
new RtspHeaders.Builder()
.addAll(
ImmutableMap.of(
RtspHeaders.CSEQ,
"4",
RtspHeaders.TRANSPORT,
"RTP/AVP;unicast;client_port=65458-65459;server_port=5354-5355"))
.build(),
/* messageBody= */ "");
List<String> messageLines = RtspMessageUtil.serializeResponse(response);
List<String> expectedLines =
Arrays.asList(
"RTSP/1.0 200 OK",
"CSeq: 4",
"Transport: RTP/AVP;unicast;client_port=65458-65459;server_port=5354-5355",
"",
"");
String expectedRtspMessage =
"RTSP/1.0 200 OK\r\n"
+ "CSeq: 4\r\n"
+ "Transport: RTP/AVP;unicast;client_port=65458-65459;server_port=5354-5355\r\n"
+ "\r\n";
assertThat(messageLines).isEqualTo(expectedLines);
assertThat(RtspMessageUtil.convertMessageToByteArray(messageLines))
.isEqualTo(expectedRtspMessage.getBytes(Charsets.UTF_8));
}
@Test
public void serialize_describeResponse_succeeds() {
RtspResponse response =
new RtspResponse(
/* status= */ 200,
new RtspHeaders.Builder()
.addAll(
ImmutableMap.of(
RtspHeaders.CSEQ, "4",
RtspHeaders.CONTENT_BASE, "rtsp://127.0.0.1/test.mkv/",
RtspHeaders.CONTENT_TYPE, "application/sdp",
RtspHeaders.CONTENT_LENGTH, "707"))
.build(),
/* messageBody= */ "v=0\r\n"
+ "o=- 1606776316530225 1 IN IP4 192.168.2.176\r\n"
+ "i=test.mkv\r\n"
+ "m=video 0 RTP/AVP 96\r\n"
+ "a=rtpmap:96 H264/90000\r\n"
+ "a=control:track1\r\n"
+ "m=audio 0 RTP/AVP 97\r\n"
+ "a=rtpmap:97 AC3/48000\r\n"
+ "a=control:track2");
List<String> messageLines = RtspMessageUtil.serializeResponse(response);
List<String> expectedLines =
Arrays.asList(
"RTSP/1.0 200 OK",
"CSeq: 4",
"Content-Base: rtsp://127.0.0.1/test.mkv/",
"Content-Type: application/sdp",
"Content-Length: 707",
"",
"v=0\r\n"
+ "o=- 1606776316530225 1 IN IP4 192.168.2.176\r\n"
+ "i=test.mkv\r\n"
+ "m=video 0 RTP/AVP 96\r\n"
+ "a=rtpmap:96 H264/90000\r\n"
+ "a=control:track1\r\n"
+ "m=audio 0 RTP/AVP 97\r\n"
+ "a=rtpmap:97 AC3/48000\r\n"
+ "a=control:track2");
String expectedRtspMessage =
"RTSP/1.0 200 OK\r\n"
+ "CSeq: 4\r\n"
+ "Content-Base: rtsp://127.0.0.1/test.mkv/\r\n"
+ "Content-Type: application/sdp\r\n"
+ "Content-Length: 707\r\n"
+ "\r\n"
+ "v=0\r\n"
+ "o=- 1606776316530225 1 IN IP4 192.168.2.176\r\n"
+ "i=test.mkv\r\n"
+ "m=video 0 RTP/AVP 96\r\n"
+ "a=rtpmap:96 H264/90000\r\n"
+ "a=control:track1\r\n"
+ "m=audio 0 RTP/AVP 97\r\n"
+ "a=rtpmap:97 AC3/48000\r\n"
+ "a=control:track2";
assertThat(messageLines).isEqualTo(expectedLines);
assertThat(RtspMessageUtil.convertMessageToByteArray(messageLines))
.isEqualTo(expectedRtspMessage.getBytes(Charsets.UTF_8));
}
@Test
public void serialize_failedResponse_succeeds() {
RtspResponse response =
new RtspResponse(
/* status= */ 454,
new RtspHeaders.Builder().add(RtspHeaders.CSEQ, "4").build(),
/* messageBody= */ "");
List<String> messageLines = RtspMessageUtil.serializeResponse(response);
List<String> expectedLines = Arrays.asList("RTSP/1.0 454 Session Not Found", "CSeq: 4", "", "");
String expectedRtspMessage = "RTSP/1.0 454 Session Not Found\r\n" + "CSeq: 4\r\n" + "\r\n";
assertThat(RtspMessageUtil.serializeResponse(response)).isEqualTo(expectedLines);
assertThat(RtspMessageUtil.convertMessageToByteArray(messageLines))
.isEqualTo(expectedRtspMessage.getBytes(Charsets.UTF_8));
}
@Test
public void removeUserInfo_withUserInfo() {
Uri uri = Uri.parse("rtsp://user:pass@foo.bar/foo.mkv");
assertThat(RtspMessageUtil.removeUserInfo(uri)).isEqualTo(Uri.parse("rtsp://foo.bar/foo.mkv"));
}
@Test
public void removeUserInfo_withUserInfoAndPortNumber() {
Uri uri = Uri.parse("rtsp://user:pass@foo.bar:5050/foo.mkv");
assertThat(RtspMessageUtil.removeUserInfo(uri))
.isEqualTo(Uri.parse("rtsp://foo.bar:5050/foo.mkv"));
}
@Test
public void removeUserInfo_withEmptyUserInfoAndPortNumber() {
Uri uri = Uri.parse("rtsp://@foo.bar:5050/foo.mkv");
assertThat(RtspMessageUtil.removeUserInfo(uri))
.isEqualTo(Uri.parse("rtsp://foo.bar:5050/foo.mkv"));
}
@Test
public void removeUserInfo_withNoUserInfo() {
Uri uri = Uri.parse("rtsp://foo.bar:5050/foo.mkv");
assertThat(RtspMessageUtil.removeUserInfo(uri))
.isEqualTo(Uri.parse("rtsp://foo.bar:5050/foo.mkv"));
}
}

View File

@ -0,0 +1,199 @@
/*
* 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 com.google.android.exoplayer2.source.rtsp.rtp;
import static com.google.android.exoplayer2.util.Util.getBytesFromHexString;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.when;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.source.rtsp.rtp.reader.RtpAc3Reader;
import com.google.android.exoplayer2.testutil.FakeTrackOutput;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.common.collect.ImmutableMap;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
/** Unit test for {@link RtpAc3Reader}. */
@RunWith(AndroidJUnit4.class)
public final class RtpAc3ReaderTest {
private final RtpPacket frame1fragment1 =
createRtpPacket(
/* timestamp= */ 2599168056L,
/* sequenceNumber= */ 40289,
/* marker= */ false,
/* payloadData= */ getBytesFromHexString("02020102"));
private final RtpPacket frame1fragment2 =
createRtpPacket(
/* timestamp= */ 2599168056L,
/* sequenceNumber= */ 40290,
/* marker= */ true,
/* payloadData= */ getBytesFromHexString("03020304"));
private final RtpPacket frame2fragment1 =
createRtpPacket(
/* timestamp= */ 2599169592L,
/* sequenceNumber= */ 40292,
/* marker= */ false,
/* payloadData= */ getBytesFromHexString("02020506"));
private final RtpPacket frame2fragment2 =
createRtpPacket(
/* timestamp= */ 2599169592L,
/* sequenceNumber= */ 40293,
/* marker= */ true,
/* payloadData= */ getBytesFromHexString("03020708"));
private static final RtpPayloadFormat AC3_FORMAT =
new RtpPayloadFormat(
new Format.Builder()
.setChannelCount(6)
.setSampleMimeType(MimeTypes.AUDIO_AC3)
.setSampleRate(48_000)
.build(),
/* rtpPayloadType= */ 97,
/* clockRate= */ 48_000,
/* fmtpParameters= */ ImmutableMap.of());
@Rule public final MockitoRule mockito = MockitoJUnit.rule();
private ParsableByteArray packetData;
private RtpAc3Reader ac3Reader;
private FakeTrackOutput trackOutput;
@Mock private ExtractorOutput extractorOutput;
@Before
public void setUp() {
packetData = new ParsableByteArray();
trackOutput = new FakeTrackOutput(/* deduplicateConsecutiveFormats= */ true);
when(extractorOutput.track(anyInt(), anyInt())).thenReturn(trackOutput);
ac3Reader = new RtpAc3Reader(AC3_FORMAT);
ac3Reader.createTracks(extractorOutput, /* trackId= */ 0);
}
@Test
public void consume_allPackets() {
ac3Reader.onReceivingFirstPacket(frame1fragment1.timestamp, frame1fragment1.sequenceNumber);
packetData.reset(frame1fragment1.payloadData);
ac3Reader.consume(
packetData,
frame1fragment1.timestamp,
frame1fragment1.sequenceNumber,
/* isFrameBoundary= */ frame1fragment1.marker);
packetData.reset(frame1fragment2.payloadData);
ac3Reader.consume(
packetData,
frame1fragment2.timestamp,
frame1fragment2.sequenceNumber,
/* isFrameBoundary= */ frame1fragment2.marker);
packetData.reset(frame2fragment1.payloadData);
ac3Reader.consume(
packetData,
frame2fragment1.timestamp,
frame2fragment1.sequenceNumber,
/* isFrameBoundary= */ frame2fragment1.marker);
packetData.reset(frame2fragment2.payloadData);
ac3Reader.consume(
packetData,
frame2fragment2.timestamp,
frame2fragment2.sequenceNumber,
/* isFrameBoundary= */ frame2fragment2.marker);
assertThat(trackOutput.getSampleCount()).isEqualTo(2);
assertThat(trackOutput.getSampleData(0)).isEqualTo(getBytesFromHexString("01020304"));
assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(0);
assertThat(trackOutput.getSampleData(1)).isEqualTo(getBytesFromHexString("05060708"));
assertThat(trackOutput.getSampleTimeUs(1)).isEqualTo(32000);
}
@Test
public void consume_fragmentedFrameMissingFirstFragment() {
// First packet timing information is transmitted over RTSP, not RTP.
ac3Reader.onReceivingFirstPacket(frame1fragment1.timestamp, frame1fragment1.sequenceNumber);
packetData.reset(frame1fragment2.payloadData);
ac3Reader.consume(
packetData,
frame1fragment2.timestamp,
frame1fragment2.sequenceNumber,
/* isFrameBoundary= */ frame1fragment2.marker);
packetData.reset(frame2fragment1.payloadData);
ac3Reader.consume(
packetData,
frame2fragment1.timestamp,
frame2fragment1.sequenceNumber,
/* isFrameBoundary= */ frame2fragment1.marker);
packetData.reset(frame2fragment2.payloadData);
ac3Reader.consume(
packetData,
frame2fragment2.timestamp,
frame2fragment2.sequenceNumber,
/* isFrameBoundary= */ frame2fragment2.marker);
assertThat(trackOutput.getSampleCount()).isEqualTo(2);
assertThat(trackOutput.getSampleData(0)).isEqualTo(getBytesFromHexString("0304"));
assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(0);
assertThat(trackOutput.getSampleData(1)).isEqualTo(getBytesFromHexString("05060708"));
assertThat(trackOutput.getSampleTimeUs(1)).isEqualTo(32000);
}
@Test
public void consume_fragmentedFrameMissingBoundaryFragment() {
ac3Reader.onReceivingFirstPacket(frame1fragment1.timestamp, frame1fragment1.sequenceNumber);
packetData.reset(frame1fragment1.payloadData);
ac3Reader.consume(
packetData,
frame1fragment1.timestamp,
frame1fragment1.sequenceNumber,
/* isFrameBoundary= */ frame1fragment1.marker);
packetData.reset(frame2fragment1.payloadData);
ac3Reader.consume(
packetData,
frame2fragment1.timestamp,
frame2fragment1.sequenceNumber,
/* isFrameBoundary= */ frame2fragment1.marker);
packetData.reset(frame2fragment2.payloadData);
ac3Reader.consume(
packetData,
frame2fragment2.timestamp,
frame2fragment2.sequenceNumber,
/* isFrameBoundary= */ frame2fragment2.marker);
assertThat(trackOutput.getSampleCount()).isEqualTo(2);
assertThat(trackOutput.getSampleData(0)).isEqualTo(getBytesFromHexString("0102"));
assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(0);
assertThat(trackOutput.getSampleData(1)).isEqualTo(getBytesFromHexString("05060708"));
assertThat(trackOutput.getSampleTimeUs(1)).isEqualTo(32000);
}
private static RtpPacket createRtpPacket(
long timestamp, int sequenceNumber, boolean marker, byte[] payloadData) {
return new RtpPacket.Builder()
.setTimestamp((int) timestamp)
.setSequenceNumber(sequenceNumber)
.setMarker(marker)
.setPayloadData(payloadData)
.build();
}
}

View File

@ -0,0 +1,213 @@
/*
* Copyright 2020 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 com.google.android.exoplayer2.source.rtsp.rtp;
import static com.google.common.truth.Truth.assertThat;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.util.ArrayList;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit tests for {@link RtpPacketReorderingQueue}. */
@RunWith(AndroidJUnit4.class)
public class RtpPacketReorderingQueueTest {
private final RtpPacketReorderingQueue reorderingQueue = new RtpPacketReorderingQueue();
@Test
public void poll_emptyQueue_returnsNull() {
assertThat(reorderingQueue.poll(/* cutoffTimestampMs= */ 0)).isNull();
}
@Test
public void poll_onlyOnePacketInQueue_returns() {
RtpPacket packet = makePacket(/* sequenceNumber= */ 1);
// Queue after offering: [1].
reorderingQueue.offer(packet, /* receivedTimestampMs= */ 0);
assertThat(reorderingQueue.poll(/* cutoffTimestampMs= */ 0)).isEqualTo(packet);
}
@Test
public void poll_withPacketsEnqueuedInOrder_returnsCorrectPacket() {
RtpPacket packet1 = makePacket(/* sequenceNumber= */ 1);
RtpPacket packet2 = makePacket(/* sequenceNumber= */ 2);
RtpPacket packet3 = makePacket(/* sequenceNumber= */ 3);
reorderingQueue.offer(packet1, /* receivedTimestampMs= */ 0);
reorderingQueue.offer(packet2, /* receivedTimestampMs= */ 0);
reorderingQueue.offer(packet3, /* receivedTimestampMs= */ 0);
assertThat(reorderingQueue.poll(/* cutoffTimestampMs= */ 0)).isEqualTo(packet1);
assertThat(reorderingQueue.poll(/* cutoffTimestampMs= */ 0)).isEqualTo(packet2);
assertThat(reorderingQueue.poll(/* cutoffTimestampMs= */ 0)).isEqualTo(packet3);
}
@Test
public void reset_nonEmptyQueue_resetsQueue() {
RtpPacket packet1 = makePacket(/* sequenceNumber= */ 1);
RtpPacket packet2 = makePacket(/* sequenceNumber= */ 2);
reorderingQueue.offer(packet1, /* receivedTimestampMs= */ 0);
reorderingQueue.offer(packet2, /* receivedTimestampMs= */ 0);
reorderingQueue.reset();
assertThat(reorderingQueue.poll(/* cutoffTimestampMs= */ 0)).isNull();
}
@Test
public void reorder_withPacketArriveOutOfOrderButInTime_returnsPacketsInCorrectOrder() {
// The packets arrive in the order of 1, 4, 5, 2, 3, 6. The mis-positioned packets (2, 3)
// arrive in time, so the polling order is: 1, null, 2, 3, 4, 5, 6.
List<RtpPacket> polledPackets = new ArrayList<>();
RtpPacket packet1 = makePacket(/* sequenceNumber= */ 1);
RtpPacket packet2 = makePacket(/* sequenceNumber= */ 2);
RtpPacket packet3 = makePacket(/* sequenceNumber= */ 3);
RtpPacket packet4 = makePacket(/* sequenceNumber= */ 4);
RtpPacket packet5 = makePacket(/* sequenceNumber= */ 5);
RtpPacket packet6 = makePacket(/* sequenceNumber= */ 6);
// Offering 1, queue after offering: [1].
reorderingQueue.offer(packet1, /* receivedTimestampMs= */ 1);
// Offering 4, queue after offering: [1, 4].
reorderingQueue.offer(packet4, /* receivedTimestampMs= */ 2);
// polling 1, queue after polling: [4].
polledPackets.add(reorderingQueue.poll(/* cutoffTimestampMs= */ 0));
// Offering 5, queue after offering: [4, 5].
reorderingQueue.offer(packet5, /* receivedTimestampMs= */ 3);
// Should not poll: still waiting for packet 2, since packet 4 is received at 2, after cutoff.
polledPackets.add(reorderingQueue.poll(/* cutoffTimestampMs= */ 1));
// Offering 2, queue after offering: [2, 4, 5].
reorderingQueue.offer(packet2, /* receivedTimestampMs= */ 4);
// polling 2, queue after polling: [4, 5].
polledPackets.add(reorderingQueue.poll(/* cutoffTimestampMs= */ 2));
// Offering 3, queue after offering: [3, 4, 5].
reorderingQueue.offer(packet3, /* receivedTimestampMs= */ 5);
// polling 3, queue after polling: [4, 5].
polledPackets.add(reorderingQueue.poll(/* cutoffTimestampMs= */ 3));
// Offering 6, queue after offering: [4, 5, 6].
reorderingQueue.offer(packet6, /* receivedTimestampMs= */ 6);
// polling 4, queue after polling: [5, 6].
polledPackets.add(reorderingQueue.poll(/* cutoffTimestampMs= */ 4));
// polling 5, queue after polling: [6].
polledPackets.add(reorderingQueue.poll(/* cutoffTimestampMs= */ 5));
// polling 6, queue after polling: [].
polledPackets.add(reorderingQueue.poll(/* cutoffTimestampMs= */ 6));
assertThat(polledPackets)
.containsExactly(packet1, null, packet2, packet3, packet4, packet5, packet6)
.inOrder();
}
@Test
public void
reorder_withPacketArriveOutOfOrderMissedDeadline_returnsPacketsWithSequenceNumberJump() {
// Packets arrive in order 1, 3, 4, 2. Packet 2 arrives after packet 3 is dequeued, so packet 2
// is discarded.
List<RtpPacket> polledPackets = new ArrayList<>();
RtpPacket packet1 = makePacket(/* sequenceNumber= */ 1);
RtpPacket packet2 = makePacket(/* sequenceNumber= */ 2);
RtpPacket packet3 = makePacket(/* sequenceNumber= */ 3);
RtpPacket packet4 = makePacket(/* sequenceNumber= */ 4);
// Offering 1, queue after offering: [1].
reorderingQueue.offer(packet1, /* receivedTimestampMs= */ 1);
// Offering 3, queue after offering: [1, 3].
reorderingQueue.offer(packet3, /* receivedTimestampMs= */ 2);
// polling 1, queue after polling: [3].
polledPackets.add(reorderingQueue.poll(/* cutoffTimestampMs= */ 1));
// Queue after offering: [3, 4].
reorderingQueue.offer(packet4, /* receivedTimestampMs= */ 3);
// Should poll packet 3 (receivedTimestampMs = 2), because 2 hasn't come after the wait.
// polling 3, queue after polling: [4].
polledPackets.add(reorderingQueue.poll(/* cutoffTimestampMs= */ 2));
// Offering 2, queue after offering: [4]. Should not add packet 2: packet 3 is already dequeued.
reorderingQueue.offer(packet2, /* receivedTimestampMs= */ 4);
// polling 4, queue after polling: [].
polledPackets.add(reorderingQueue.poll(/* cutoffTimestampMs= */ 3));
assertThat(polledPackets).containsExactly(packet1, packet3, packet4).inOrder();
}
@Test
public void reorder_withLargerThanAllowedJumpForwardInSequenceNumber_resetsQueue() {
RtpPacket packet1 = makePacket(/* sequenceNumber= */ 1);
RtpPacket packetWithSequenceNumberJump =
makePacket(/* sequenceNumber= */ 10 + RtpPacketReorderingQueue.MAX_SEQUENCE_LEAP_ALLOWED);
// Offering 1, queue after offering: [1].
reorderingQueue.offer(packet1, /* receivedTimestampMs= */ 1);
// Queueing a packet with a sequence number that creates a shift larger than the allowed maximum
// will force reset the queue.
reorderingQueue.offer(packetWithSequenceNumberJump, /* receivedTimestampMs= */ 2);
assertThat(reorderingQueue.poll(/* cutoffTimestampMs= */ 0))
.isEqualTo(packetWithSequenceNumberJump);
}
@Test
public void reorder_withLargerThanAllowedJumpInSequenceNumberAndWrapAround_resetsQueue() {
RtpPacket packet1 = makePacket(/* sequenceNumber= */ 1);
RtpPacket packetWithSequenceNumberJump =
makePacket(
/* sequenceNumber= */ RtpPacket.MAX_SEQUENCE_NUMBER
- RtpPacketReorderingQueue.MAX_SEQUENCE_LEAP_ALLOWED
- 10);
// Offering 1, queue after offering: [1].
reorderingQueue.offer(packet1, /* receivedTimestampMs= */ 1);
// Queueing a packet with a sequence number that creates a shift larger than the allowed maximum
// will force reset the queue.
reorderingQueue.offer(packetWithSequenceNumberJump, /* receivedTimestampMs= */ 2);
assertThat(reorderingQueue.poll(/* cutoffTimestampMs= */ 0))
.isEqualTo(packetWithSequenceNumberJump);
}
@Test
public void reorder_receivingOutOfOrderPacketWithWrapAround_returnsPacketsInCorrectOrder() {
RtpPacket packet2 = makePacket(/* sequenceNumber= */ 2);
RtpPacket packet3 = makePacket(/* sequenceNumber= */ 3);
RtpPacket packet65000 = makePacket(/* sequenceNumber= */ 65000);
RtpPacket packet65001 = makePacket(/* sequenceNumber= */ 65001);
RtpPacket packet65002 = makePacket(/* sequenceNumber= */ 65002);
RtpPacket packet65003 = makePacket(/* sequenceNumber= */ 65003);
RtpPacket packet65004 = makePacket(/* sequenceNumber= */ 65004);
reorderingQueue.offer(packet65000, /* receivedTimestampMs= */ 1);
reorderingQueue.offer(packet65001, /* receivedTimestampMs= */ 2);
reorderingQueue.offer(packet65002, /* receivedTimestampMs= */ 3);
reorderingQueue.offer(packet65003, /* receivedTimestampMs= */ 4);
assertThat(reorderingQueue.poll(/* cutoffTimestampMs= */ 0)).isEqualTo(packet65000);
reorderingQueue.offer(packet2, /* receivedTimestampMs= */ 5);
reorderingQueue.offer(packet3, /* receivedTimestampMs= */ 6);
reorderingQueue.offer(packet65004, /* receivedTimestampMs= */ 7);
assertThat(reorderingQueue.poll(/* cutoffTimestampMs= */ 0)).isEqualTo(packet65001);
assertThat(reorderingQueue.poll(/* cutoffTimestampMs= */ 0)).isEqualTo(packet65002);
assertThat(reorderingQueue.poll(/* cutoffTimestampMs= */ 0)).isEqualTo(packet65003);
assertThat(reorderingQueue.poll(/* cutoffTimestampMs= */ 0)).isEqualTo(packet65004);
assertThat(reorderingQueue.poll(/* cutoffTimestampMs= */ 6)).isEqualTo(packet2);
assertThat(reorderingQueue.poll(/* cutoffTimestampMs= */ 0)).isEqualTo(packet3);
}
private static RtpPacket makePacket(int sequenceNumber) {
return new RtpPacket.Builder().setSequenceNumber(sequenceNumber).build();
}
}

View File

@ -0,0 +1,187 @@
/*
* Copyright 2020 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 com.google.android.exoplayer2.source.rtsp.rtp;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.android.exoplayer2.util.Util.getBytesFromHexString;
import static com.google.common.truth.Truth.assertThat;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import java.util.Arrays;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit tests for {@link RtpPacket}. */
@RunWith(AndroidJUnit4.class)
public final class RtpPacketTest {
/*
10.. .... = Version: RFC 1889 Version (2)
..0. .... = Padding: False
...0 .... = Extension: False
.... 0000 = Contributing source identifiers count: 0
1... .... = Marker: True
Payload type: DynamicRTP-Type-96 (96)
Sequence number: 22159
Timestamp: 55166400
Synchronization Source identifier: 0xd76ef1a6 (3614372262)
Payload: 019fb174427f00006c10c4008962e33ceb5f1fde8ee2d0d9
*/
private final byte[] rtpData =
getBytesFromHexString(
"80e0568f0349c5c0d76ef1a6019fb174427f00006c10c4008962e33ceb5f1fde8ee2d0d9b169651024c83b24c3a0f274ea327e2440ae0d3e2ed194beaa2c91edaa5d1e1df7ce30d1ca3726804d2db37765cf3d174338459623bc627c15c687045390a8d702f623a8dbe49e5c7896dbd7105daecb02ce30c0eee324c0c21ed820a0e67344c7a6e10859");
private final byte[] rtpPayloadData =
Arrays.copyOfRange(rtpData, RtpPacket.MIN_HEADER_SIZE, rtpData.length);
/*
10.. .... = Version: RFC 1889 Version (2)
..0. .... = Padding: False
...0 .... = Extension: False
.... 0000 = Contributing source identifiers count: 0
1... .... = Marker: True
Payload type: DynamicRTP-Type-96 (96)
Sequence number: 29234
Timestamp: 3688686074
Synchronization Source identifier: 0xf5fe62a4 (4127089316)
Payload: 419a246c43bffea996000003000003000003000003000003
*/
private final byte[] rtpDataWithLargeTimestamp =
getBytesFromHexString(
"80e07232dbdce1faf5fe62a4419a246c43bffea99600000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300002ce0");
private final byte[] rtpWithLargeTimestampPayloadData =
Arrays.copyOfRange(
rtpDataWithLargeTimestamp, RtpPacket.MIN_HEADER_SIZE, rtpDataWithLargeTimestamp.length);
@Test
public void parseRtpPacket() {
RtpPacket packet = checkNotNull(RtpPacket.parse(rtpData, rtpData.length));
assertThat(packet.version).isEqualTo(RtpPacket.RTP_VERSION);
assertThat(packet.padding).isFalse();
assertThat(packet.extension).isFalse();
assertThat(packet.csrcCount).isEqualTo(0);
assertThat(packet.csrc).hasLength(0);
assertThat(packet.marker).isTrue();
assertThat(packet.payloadType).isEqualTo(96);
assertThat(packet.sequenceNumber).isEqualTo(22159);
assertThat(packet.timestamp).isEqualTo(55166400);
assertThat(packet.ssrc).isEqualTo(0xD76EF1A6);
assertThat(packet.payloadData).isEqualTo(rtpPayloadData);
}
@Test
public void parseRtpPacketWithLargeTimestamp() {
RtpPacket packet =
checkNotNull(RtpPacket.parse(rtpDataWithLargeTimestamp, rtpDataWithLargeTimestamp.length));
assertThat(packet.version).isEqualTo(RtpPacket.RTP_VERSION);
assertThat(packet.padding).isFalse();
assertThat(packet.extension).isFalse();
assertThat(packet.csrcCount).isEqualTo(0);
assertThat(packet.csrc).hasLength(0);
assertThat(packet.marker).isTrue();
assertThat(packet.payloadType).isEqualTo(96);
assertThat(packet.sequenceNumber).isEqualTo(29234);
assertThat(packet.timestamp).isEqualTo(3688686074L);
assertThat(packet.ssrc).isEqualTo(0xf5fe62a4);
assertThat(packet.payloadData).isEqualTo(rtpWithLargeTimestampPayloadData);
}
@Test
public void writetoBuffer_withProperlySizedBuffer_writesPacket() {
int packetByteLength = rtpData.length;
RtpPacket packet = checkNotNull(RtpPacket.parse(rtpData, packetByteLength));
byte[] testBuffer = new byte[packetByteLength];
int writtenBytes = packet.writeToBuffer(testBuffer, /* offset= */ 0, packetByteLength);
assertThat(writtenBytes).isEqualTo(packetByteLength);
assertThat(testBuffer).isEqualTo(rtpData);
}
@Test
public void writetoBuffer_withBufferTooSmall_doesNotWritePacket() {
int packetByteLength = rtpData.length;
RtpPacket packet = checkNotNull(RtpPacket.parse(rtpData, packetByteLength));
byte[] testBuffer = new byte[packetByteLength / 2];
int writtenBytes = packet.writeToBuffer(testBuffer, /* offset= */ 0, packetByteLength);
assertThat(writtenBytes).isEqualTo(C.LENGTH_UNSET);
}
@Test
public void writetoBuffer_withProperlySizedBufferButSmallLengthParameter_doesNotWritePacket() {
int packetByteLength = rtpData.length;
RtpPacket packet = checkNotNull(RtpPacket.parse(rtpData, packetByteLength));
byte[] testBuffer = new byte[packetByteLength];
int writtenBytes = packet.writeToBuffer(testBuffer, /* offset= */ 0, packetByteLength / 2);
assertThat(writtenBytes).isEqualTo(C.LENGTH_UNSET);
}
@Test
public void writetoBuffer_withProperlySizedBufferButNotEnoughSpaceLeft_doesNotWritePacket() {
int packetByteLength = rtpData.length;
RtpPacket packet = checkNotNull(RtpPacket.parse(rtpData, packetByteLength));
byte[] testBuffer = new byte[packetByteLength];
int writtenBytes =
packet.writeToBuffer(testBuffer, /* offset= */ packetByteLength - 1, packetByteLength);
assertThat(writtenBytes).isEqualTo(C.LENGTH_UNSET);
}
@Test
public void buildRtpPacket() {
RtpPacket builtPacket =
new RtpPacket.Builder()
.setPadding(false)
.setMarker(true)
.setPayloadType((byte) 96)
.setSequenceNumber(22159)
.setTimestamp(55166400)
.setSsrc(0xD76EF1A6)
.setPayloadData(rtpPayloadData)
.build();
RtpPacket parsedPacket = checkNotNull(RtpPacket.parse(rtpData, rtpData.length));
// Test equals function.
assertThat(parsedPacket).isEqualTo(builtPacket);
}
@Test
public void buildRtpPacketWithLargeTimestamp_matchesPacketData() {
RtpPacket builtPacket =
new RtpPacket.Builder()
.setPadding(false)
.setMarker(true)
.setPayloadType((byte) 96)
.setSequenceNumber(29234)
.setTimestamp(3688686074L)
.setSsrc(0xf5fe62a4)
.setPayloadData(rtpWithLargeTimestampPayloadData)
.build();
int packetSize = RtpPacket.MIN_HEADER_SIZE + builtPacket.payloadData.length;
byte[] builtPacketBytes = new byte[packetSize];
builtPacket.writeToBuffer(builtPacketBytes, /* offset= */ 0, packetSize);
assertThat(builtPacketBytes).isEqualTo(rtpDataWithLargeTimestamp);
}
}

View File

@ -0,0 +1,173 @@
/*
* 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 com.google.android.exoplayer2.source.rtsp.sdp;
import static com.google.android.exoplayer2.source.rtsp.sdp.MediaDescription.MEDIA_TYPE_AUDIO;
import static com.google.android.exoplayer2.source.rtsp.sdp.MediaDescription.MEDIA_TYPE_VIDEO;
import static com.google.android.exoplayer2.source.rtsp.sdp.MediaDescription.RTP_AVP_PROFILE;
import static com.google.android.exoplayer2.source.rtsp.sdp.SessionDescription.ATTR_CONTROL;
import static com.google.android.exoplayer2.source.rtsp.sdp.SessionDescription.ATTR_FMTP;
import static com.google.android.exoplayer2.source.rtsp.sdp.SessionDescription.ATTR_RANGE;
import static com.google.android.exoplayer2.source.rtsp.sdp.SessionDescription.ATTR_RTPMAP;
import static com.google.android.exoplayer2.source.rtsp.sdp.SessionDescription.ATTR_TOOL;
import static com.google.android.exoplayer2.source.rtsp.sdp.SessionDescription.ATTR_TYPE;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import android.net.Uri;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit test for {@link SessionDescription}. */
@RunWith(AndroidJUnit4.class)
public class SessionDescriptionTest {
@Test
public void parse_sdpString_succeeds() throws Exception {
String testMediaSdpInfo =
"v=0\r\n"
+ "o=MNobody 2890844526 2890842807 IN IP4 192.0.2.46\r\n"
+ "s=SDP Seminar\r\n"
+ "i=A Seminar on the session description protocol\r\n"
+ "u=http://www.example.com/lectures/sdp.ps\r\n"
+ "e=seminar@example.com (Seminar Management)\r\n"
+ "c=IN IP4 0.0.0.0\r\n"
+ "a=control:*\r\n"
+ "t=2873397496 2873404696\r\n"
+ "m=audio 3456 RTP/AVP 0\r\n"
+ "a=control:audio\r\n"
+ "a=rtpmap:0 PCMU/8000\r\n"
+ "a=3GPP-Adaption-Support:1\r\n"
+ "m=video 2232 RTP/AVP 31\r\n"
+ "a=control:video\r\n"
+ "a=rtpmap:31 H261/90000\r\n";
SessionDescription sessionDescription = SessionDescriptionParser.parse(testMediaSdpInfo);
SessionDescription expectedSession =
new SessionDescription.Builder()
.setOrigin("MNobody 2890844526 2890842807 IN IP4 192.0.2.46")
.setSessionName("SDP Seminar")
.setSessionInfo("A Seminar on the session description protocol")
.setUri(Uri.parse("http://www.example.com/lectures/sdp.ps"))
.setEmailAddress("seminar@example.com (Seminar Management)")
.setConnection("IN IP4 0.0.0.0")
.setTiming("2873397496 2873404696")
.addAttribute(ATTR_CONTROL, "*")
.addMediaDescription(
new MediaDescription.Builder(MEDIA_TYPE_AUDIO, 3456, RTP_AVP_PROFILE, 0)
.addAttribute(ATTR_CONTROL, "audio")
.addAttribute(ATTR_RTPMAP, "0 PCMU/8000")
.addAttribute("3GPP-Adaption-Support", "1")
.build())
.addMediaDescription(
new MediaDescription.Builder(MEDIA_TYPE_VIDEO, 2232, RTP_AVP_PROFILE, 31)
.addAttribute(ATTR_CONTROL, "video")
.addAttribute(ATTR_RTPMAP, "31 H261/90000")
.build())
.build();
assertThat(sessionDescription).isEqualTo(expectedSession);
}
@Test
public void parse_sdpString2_succeeds() throws Exception {
String testMediaSdpInfo =
"v=0\r\n"
+ "o=- 1600785369059721 1 IN IP4 192.168.2.176\r\n"
+ "s=video+audio, streamed by ExoPlayer\r\n"
+ "i=test.mkv\r\n"
+ "t=0 0\r\n"
+ "a=tool:ExoPlayer\r\n"
+ "a=type:broadcast\r\n"
+ "a=control:*\r\n"
+ "a=range:npt=0-30.102\r\n"
+ "m=video 0 RTP/AVP 96\r\n"
+ "c=IN IP4 0.0.0.0\r\n"
+ "b=AS:500\r\n"
+ "a=rtpmap:96 H264/90000\r\n"
+ "a=fmtp:96"
+ " packetization-mode=1;profile-level-id=64001F;sprop-parameter-sets=Z2QAH6zZQPARabIAAAMACAAAAwGcHjBjLA==,aOvjyyLAAA==\r\n"
+ "a=control:track1\r\n"
+ "m=audio 0 RTP/AVP 97\r\n"
+ "c=IN IP4 0.0.0.0\r\n"
+ "b=AS:96\r\n"
+ "a=rtpmap:97 MPEG4-GENERIC/44100\r\n"
+ "a=fmtp:97"
+ " streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1208\r\n"
+ "a=control:track2\r\n";
SessionDescription sessionDescription = SessionDescriptionParser.parse(testMediaSdpInfo);
SessionDescription expectedSession =
new SessionDescription.Builder()
.setOrigin("- 1600785369059721 1 IN IP4 192.168.2.176")
.setSessionName("video+audio, streamed by ExoPlayer")
.setSessionInfo("test.mkv")
.setTiming("0 0")
.addAttribute(ATTR_TOOL, "ExoPlayer")
.addAttribute(ATTR_TYPE, "broadcast")
.addAttribute(ATTR_CONTROL, "*")
.addAttribute(ATTR_RANGE, "npt=0-30.102")
.addMediaDescription(
new MediaDescription.Builder(MEDIA_TYPE_VIDEO, 0, RTP_AVP_PROFILE, 96)
.setConnection("IN IP4 0.0.0.0")
.setBitrate(500_000)
.addAttribute(ATTR_RTPMAP, "96 H264/90000")
.addAttribute(
ATTR_FMTP,
"96 packetization-mode=1;profile-level-id=64001F;sprop-parameter-sets=Z2QAH6zZQPARabIAAAMACAAAAwGcHjBjLA==,aOvjyyLAAA==")
.addAttribute(ATTR_CONTROL, "track1")
.build())
.addMediaDescription(
new MediaDescription.Builder(MEDIA_TYPE_AUDIO, 0, RTP_AVP_PROFILE, 97)
.setConnection("IN IP4 0.0.0.0")
.setBitrate(96_000)
.addAttribute(ATTR_RTPMAP, "97 MPEG4-GENERIC/44100")
.addAttribute(
ATTR_FMTP,
"97 streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1208")
.addAttribute(ATTR_CONTROL, "track2")
.build())
.build();
assertThat(sessionDescription).isEqualTo(expectedSession);
}
@Test
public void buildMediaDescription_withInvalidRtpmapAttribute_throwsIllegalStateException() {
assertThrows(
IllegalStateException.class,
() ->
new MediaDescription.Builder(
MEDIA_TYPE_AUDIO, /* port= */ 0, RTP_AVP_PROFILE, /* payloadType= */ 97)
.addAttribute(ATTR_RTPMAP, "AF AC3/44100")
.build());
}
@Test
public void buildMediaDescription_withInvalidRtpmapAttribute2_throwsIllegalStateException() {
assertThrows(
IllegalStateException.class,
() ->
new MediaDescription.Builder(
MEDIA_TYPE_AUDIO, /* port= */ 0, RTP_AVP_PROFILE, /* payloadType= */ 97)
.addAttribute(ATTR_RTPMAP, "97 AC3/441A0")
.build());
}
}