Publish ExoPlayer's support for RTSP.
Allow ExoPlayer to open URIs starting with rtsp:// PiperOrigin-RevId: 370653248
This commit is contained in:
parent
74de77a1b6
commit
8135b9c273
@ -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)
|
||||
|
||||
|
@ -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')
|
||||
|
@ -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.
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>();
|
||||
}
|
||||
|
@ -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
49
library/rtsp/build.gradle
Normal 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'
|
17
library/rtsp/src/main/AndroidManifest.xml
Normal file
17
library/rtsp/src/main/AndroidManifest.xml
Normal 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"/>
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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 <headerName>:
|
||||
* <headerValue>
|
||||
* @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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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() {}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
@ -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;
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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;
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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() {}
|
||||
}
|
@ -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;
|
19
library/rtsp/src/test/AndroidManifest.xml
Normal file
19
library/rtsp/src/test/AndroidManifest.xml
Normal 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>
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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"));
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
@ -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"));
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user