Commit 8135b9c2 by claincly Committed by bachinger

Publish ExoPlayer's support for RTSP.

Allow ExoPlayer to open URIs starting with rtsp://

PiperOrigin-RevId: 370653248
parent 74de77a1
Showing with 4485 additions and 4 deletions
...@@ -132,6 +132,8 @@ ...@@ -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' ...@@ -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 ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 @@ ...@@ -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 { ...@@ -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));
......
// 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'
<?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"/>
/*
* 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));
}
}
}
}
/*
* 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;
}
}
/*
* 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;
}
}
/*
* 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;
}
}
/*
* 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;
}
}
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.source.rtsp.message;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.util.Util;
import com.google.common.base.Ascii;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* RTSP message headers.
*
* <p>{@link Builder} must be used to construct an instance. Use {@link #get} to query header values
* with case-insensitive header names. The extra spaces around header names and values are trimmed.
* Contrary to HTTP, RTSP does not allow ambiguous/arbitrary header names (RFC 2326 Section 12).
*/
public final class RtspHeaders {
public static final String ACCEPT = "Accept";
public static final String ALLOW = "Allow";
public static final String AUTHORIZATION = "Authorization";
public static final String BANDWIDTH = "Bandwidth";
public static final String BLOCKSIZE = "Blocksize";
public static final String CACHE_CONTROL = "Cache-Control";
public static final String CONNECTION = "Connection";
public static final String CONTENT_BASE = "Content-Base";
public static final String CONTENT_ENCODING = "Content-Encoding";
public static final String CONTENT_LANGUAGE = "Content-Language";
public static final String CONTENT_LENGTH = "Content-Length";
public static final String CONTENT_LOCATION = "Content-Location";
public static final String CONTENT_TYPE = "Content-Type";
public static final String CSEQ = "CSeq";
public static final String DATE = "Date";
public static final String EXPIRES = "Expires";
public static final String PROXY_AUTHENTICATE = "Proxy-Authenticate";
public static final String PROXY_REQUIRE = "Proxy-Require";
public static final String PUBLIC = "Public";
public static final String RANGE = "Range";
public static final String RTP_INFO = "RTP-Info";
public static final String RTCP_INTERVAL = "RTCP-Interval";
public static final String SCALE = "Scale";
public static final String SESSION = "Session";
public static final String SPEED = "Speed";
public static final String SUPPORTED = "Supported";
public static final String TIMESTAMP = "Timestamp";
public static final String TRANSPORT = "Transport";
public static final String USER_AGENT = "User-Agent";
public static final String VIA = "Via";
public static final String WWW_AUTHENTICATE = "WWW-Authenticate";
/** Builds {@link RtspHeaders} instances. */
public static final class Builder {
private final List<String> namesAndValues;
/** Creates a new instance. */
public Builder() {
namesAndValues = new ArrayList<>();
}
/**
* Adds a header name and header value pair.
*
* @param headerName The name of the header.
* @param headerValue The value of the header.
* @return This builder.
*/
public Builder add(String headerName, String headerValue) {
namesAndValues.add(headerName.trim());
namesAndValues.add(headerValue.trim());
return this;
}
/**
* Adds a list of headers.
*
* @param headers The list of headers, each item must following the format &lt;headerName&gt;:
* &lt;headerValue&gt;
* @return This builder.
*/
public Builder addAll(List<String> headers) {
for (int i = 0; i < headers.size(); i++) {
String[] header = Util.splitAtFirst(headers.get(i), ":\\s?");
if (header.length == 2) {
add(header[0], header[1]);
}
}
return this;
}
/**
* Adds multiple headers in a map.
*
* @param headers The map of headers, where the keys are the header names and the values are the
* header values.
* @return This builder.
*/
public Builder addAll(Map<String, String> headers) {
for (Map.Entry<String, String> header : headers.entrySet()) {
add(header.getKey(), header.getValue());
}
return this;
}
/**
* Builds a new {@link RtspHeaders} instance.
*
* @return The newly built {@link RtspHeaders} instance.
*/
public RtspHeaders build() {
return new RtspHeaders(this);
}
}
private final ImmutableList<String> namesAndValues;
/**
* Gets the headers as a map, where the keys are the header names and values are the header
* values.
*
* @return The headers as a map. The keys of the map have follows those that are used to build
* this {@link RtspHeaders} instance.
*/
public ImmutableMap<String, String> asMap() {
Map<String, String> headers = new LinkedHashMap<>();
for (int i = 0; i < namesAndValues.size(); i += 2) {
headers.put(namesAndValues.get(i), namesAndValues.get(i + 1));
}
return ImmutableMap.copyOf(headers);
}
/**
* Returns a header value mapped to the argument, {@code null} if the header name is not recorded.
*/
@Nullable
public String get(String headerName) {
for (int i = namesAndValues.size() - 2; i >= 0; i -= 2) {
if (Ascii.equalsIgnoreCase(headerName, namesAndValues.get(i))) {
return namesAndValues.get(i + 1);
}
}
return null;
}
private RtspHeaders(Builder builder) {
this.namesAndValues = ImmutableList.copyOf(builder.namesAndValues);
}
}
/*
* 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;
}
}
}
/*
* 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);
}
}
/*
* 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);
}
}
/*
* 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;
}
}
/*
* 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;
}
}
/*
* 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;
}
}
/*
* 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;
/*
* 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;
/*
* 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;
}
}
/*
* 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;
}
}
/*
* 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;
}
}
/*
* 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;
}
}
/*
* 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;
/*
* 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;
}
}
/*
* 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);
}
}
/*
* 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);
}
}
/*
* 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);
}
/*
* 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;
/*
* 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() {}
}
/*
* 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;
<?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>
/*
* 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);
}
}
/*
* 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);
}
}
/*
* 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();
}
}
}
}
/*
* 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"));
}
}
/*
* 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));
}
}
/*
* 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");
}
}
/*
* 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();
}
}
/*
* 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);
}
}
/*
* 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());
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment