mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
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`.
|
`DashMediaSource.Factory`.
|
||||||
* We don't currently support using platform extractors with
|
* We don't currently support using platform extractors with
|
||||||
SmoothStreaming.
|
SmoothStreaming.
|
||||||
|
* RTSP
|
||||||
|
* Release the initial version of ExoPlayer's RTSP support.
|
||||||
|
|
||||||
### 2.13.3 (2021-04-14)
|
### 2.13.3 (2021-04-14)
|
||||||
|
|
||||||
|
@ -27,6 +27,7 @@ include modulePrefix + 'library-core'
|
|||||||
include modulePrefix + 'library-dash'
|
include modulePrefix + 'library-dash'
|
||||||
include modulePrefix + 'library-extractor'
|
include modulePrefix + 'library-extractor'
|
||||||
include modulePrefix + 'library-hls'
|
include modulePrefix + 'library-hls'
|
||||||
|
include modulePrefix + 'library-rtsp'
|
||||||
include modulePrefix + 'library-smoothstreaming'
|
include modulePrefix + 'library-smoothstreaming'
|
||||||
include modulePrefix + 'library-transformer'
|
include modulePrefix + 'library-transformer'
|
||||||
include modulePrefix + 'library-ui'
|
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-dash').projectDir = new File(rootDir, 'library/dash')
|
||||||
project(modulePrefix + 'library-extractor').projectDir = new File(rootDir, 'library/extractor')
|
project(modulePrefix + 'library-extractor').projectDir = new File(rootDir, 'library/extractor')
|
||||||
project(modulePrefix + 'library-hls').projectDir = new File(rootDir, 'library/hls')
|
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-smoothstreaming').projectDir = new File(rootDir, 'library/smoothstreaming')
|
||||||
project(modulePrefix + 'library-transformer').projectDir = new File(rootDir, 'library/transformer')
|
project(modulePrefix + 'library-transformer').projectDir = new File(rootDir, 'library/transformer')
|
||||||
project(modulePrefix + 'library-ui').projectDir = new File(rootDir, 'library/ui')
|
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
|
* 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
|
@Documented
|
||||||
@Retention(RetentionPolicy.SOURCE)
|
@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 {}
|
public @interface ContentType {}
|
||||||
/**
|
/**
|
||||||
* Value returned by {@link Util#inferContentType(String)} for DASH manifests.
|
* 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.
|
* Value returned by {@link Util#inferContentType(String)} for HLS manifests.
|
||||||
*/
|
*/
|
||||||
public static final int TYPE_HLS = 2;
|
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
|
* 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.
|
* 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_EXIF = BASE_TYPE_APPLICATION + "/x-exif";
|
||||||
public static final String APPLICATION_ICY = BASE_TYPE_APPLICATION + "/x-icy";
|
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_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";
|
public static final String IMAGE_JPEG = BASE_TYPE_IMAGE + "/jpeg";
|
||||||
|
|
||||||
|
@ -1800,6 +1800,11 @@ public final class Util {
|
|||||||
*/
|
*/
|
||||||
@ContentType
|
@ContentType
|
||||||
public static int inferContentType(Uri uri) {
|
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();
|
@Nullable String path = uri.getPath();
|
||||||
return path == null ? C.TYPE_OTHER : inferContentType(path);
|
return path == null ? C.TYPE_OTHER : inferContentType(path);
|
||||||
}
|
}
|
||||||
@ -1852,6 +1857,8 @@ public final class Util {
|
|||||||
return C.TYPE_HLS;
|
return C.TYPE_HLS;
|
||||||
case MimeTypes.APPLICATION_SS:
|
case MimeTypes.APPLICATION_SS:
|
||||||
return C.TYPE_SS;
|
return C.TYPE_SS;
|
||||||
|
case MimeTypes.APPLICATION_RTSP:
|
||||||
|
return C.TYPE_RTSP;
|
||||||
default:
|
default:
|
||||||
return C.TYPE_OTHER;
|
return C.TYPE_OTHER;
|
||||||
}
|
}
|
||||||
|
@ -64,3 +64,7 @@
|
|||||||
-keepclasseswithmembers class com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory {
|
-keepclasseswithmembers class com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory {
|
||||||
<init>(com.google.android.exoplayer2.upstream.DataSource$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) {
|
} catch (Exception e) {
|
||||||
// Expected if the app was built without the hls module.
|
// 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)
|
// LINT.ThenChange(../../../../../../../../proguard-rules.txt)
|
||||||
factories.put(
|
factories.put(
|
||||||
C.TYPE_OTHER, new ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory));
|
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