From cf554ac5733a1b54d99266decce52a7ab9337749 Mon Sep 17 00:00:00 2001 From: Sergio Moreno Date: Wed, 6 Sep 2017 15:29:29 +0200 Subject: [PATCH] Added RTP DataSource supporting Fast Multicast Acquisition and RTP packet retransmission, only ALU/Nokia implementation is included but is extensible to other standards as the architecture is modelled following the RFC6285. In order to use this Datasource for unicast streams you will need an RTSP datasource not yet included in this pull request. --- demos/cast/build.gradle | 8 +- demos/main/src/main/AndroidManifest.xml | 3 + .../exoplayer2/demo/DemoApplication.java | 93 ++ .../exoplayer2/demo/PlayerActivity.java | 41 +- .../demo/SampleChooserActivity.java | 34 +- library/core/src/main/AndroidManifest.xml | 2 +- .../exoplayer2/upstream/RtpDataSource.java | 1101 ++++++++++++++ .../upstream/RtpDataSourceFactory.java | 57 + .../exoplayer2/util/net/Connectivity.java | 141 ++ .../exoplayer2/util/net/NetworkUtils.java | 82 + .../util/rtp/AluRtpDistributionFeedback.java | 852 +++++++++++ .../AluRtpDistributionFeedbackFactory.java | 32 + .../rtp/DefaultRtpDistributionFeedback.java | 377 +++++ ...DefaultRtpDistributionFeedbackFactory.java | 32 + .../util/rtp/RtpDistributionFeedback.java | 1234 +++++++++++++++ .../util/rtp/RtpExtractorsFactory.java | 41 + .../exoplayer2/util/rtp/RtpPacket.java | 328 ++++ .../exoplayer2/util/rtp/RtpPacketQueue.java | 137 ++ .../util/rtp/rtcp/RtcpCompoundPacket.java | 82 + .../util/rtp/rtcp/RtcpFeedbackPacket.java | 27 + .../exoplayer2/util/rtp/rtcp/RtcpPacket.java | 167 +++ .../util/rtp/rtcp/RtcpPacketBuilder.java | 1323 +++++++++++++++++ .../util/rtp/rtcp/RtcpPacketUtils.java | 157 ++ .../util/rtp/rtcp/RtcpSdesPacket.java | 23 + .../util/rtp/rtcp/RtcpSessionUtils.java | 55 + .../util/rtp/rtcp/RtcpSrPacket.java | 23 + .../util/rtp/rtcp/RtcpTokenPacket.java | 31 + 27 files changed, 6471 insertions(+), 12 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/upstream/RtpDataSource.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/upstream/RtpDataSourceFactory.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/util/net/Connectivity.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/util/net/NetworkUtils.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/util/rtp/AluRtpDistributionFeedback.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/util/rtp/AluRtpDistributionFeedbackFactory.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/util/rtp/DefaultRtpDistributionFeedback.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/util/rtp/DefaultRtpDistributionFeedbackFactory.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/util/rtp/RtpDistributionFeedback.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/util/rtp/RtpExtractorsFactory.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/util/rtp/RtpPacket.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/util/rtp/RtpPacketQueue.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/util/rtp/rtcp/RtcpCompoundPacket.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/util/rtp/rtcp/RtcpFeedbackPacket.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/util/rtp/rtcp/RtcpPacket.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/util/rtp/rtcp/RtcpPacketBuilder.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/util/rtp/rtcp/RtcpPacketUtils.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/util/rtp/rtcp/RtcpSdesPacket.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/util/rtp/rtcp/RtcpSessionUtils.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/util/rtp/rtcp/RtcpSrPacket.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/util/rtp/rtcp/RtcpTokenPacket.java diff --git a/demos/cast/build.gradle b/demos/cast/build.gradle index a9fa27ad586..bd0464fd7e0 100644 --- a/demos/cast/build.gradle +++ b/demos/cast/build.gradle @@ -15,7 +15,7 @@ apply from: '../../constants.gradle' apply plugin: 'com.android.application' android { - compileSdkVersion project.ext.compileSdkVersion + compileSdkVersion 25 buildToolsVersion project.ext.buildToolsVersion defaultConfig { @@ -42,10 +42,4 @@ android { } dependencies { - compile project(modulePrefix + 'library-core') - compile project(modulePrefix + 'library-dash') - compile project(modulePrefix + 'library-hls') - compile project(modulePrefix + 'library-smoothstreaming') - compile project(modulePrefix + 'library-ui') - compile project(modulePrefix + 'extension-cast') } diff --git a/demos/main/src/main/AndroidManifest.xml b/demos/main/src/main/AndroidManifest.xml index 4f90cef623d..2299fa3c0d0 100644 --- a/demos/main/src/main/AndroidManifest.xml +++ b/demos/main/src/main/AndroidManifest.xml @@ -21,6 +21,9 @@ + + + diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java index b5db4c018d4..e000e3f68b0 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java @@ -16,12 +16,18 @@ package com.google.android.exoplayer2.demo; import android.app.Application; +import android.util.Log; + import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; import com.google.android.exoplayer2.upstream.HttpDataSource; +import com.google.android.exoplayer2.upstream.RtpDataSource; +import com.google.android.exoplayer2.upstream.RtpDataSourceFactory; import com.google.android.exoplayer2.util.Util; +import com.google.android.exoplayer2.util.rtp.AluRtpDistributionFeedback; +import com.google.android.exoplayer2.util.rtp.RtpDistributionFeedback; /** * Placeholder application to facilitate overriding Application methods for debugging and testing. @@ -30,6 +36,50 @@ public class DemoApplication extends Application { protected String userAgent; + protected RtpDistributionFeedback.RtpFeedbackEventListener eventListener = + new RtpDistributionFeedback.RtpFeedbackEventListener () { + + @Override + public void onRtpFeedbackEvent( + RtpDistributionFeedback.RtpFeedbackEvent event) { + + if (event instanceof AluRtpDistributionFeedback.AluRtpFeedbackConfigDiscoveryStarted) { + + Log.v("AluRtpFeedback", "ALU RTP Feedback Configuration Discovery Started"); + } + else if (event instanceof AluRtpDistributionFeedback.AluRtpFeedbackConfigDiscoveryEnded) { + + Log.v("AluRtpFeedback", "ALU RTP Feedback Configuration Discovery Ended"); + } + else if (event instanceof AluRtpDistributionFeedback.AluDefaultRtpBurstServerEvent) { + + AluRtpDistributionFeedback.AluDefaultRtpBurstServerEvent serverEvent = + (AluRtpDistributionFeedback.AluDefaultRtpBurstServerEvent) event; + + Log.v("AluRtpFeedback", "default burst=[" + serverEvent.getBurstServer() + "]"); + } + else if (event instanceof AluRtpDistributionFeedback.AluDefaultRtpRetransmissionServerEvent) { + + AluRtpDistributionFeedback.AluDefaultRtpRetransmissionServerEvent serverEvent = + (AluRtpDistributionFeedback.AluDefaultRtpRetransmissionServerEvent) event; + + Log.v("AluRtpFeedback", "default retransmission=[" + serverEvent.getRetransmissionServer() + "]"); + } + else if (event instanceof AluRtpDistributionFeedback.AluRtpMulticastGroupInfoEvent) { + + AluRtpDistributionFeedback.AluRtpMulticastGroupInfoEvent groupInfo = + (AluRtpDistributionFeedback.AluRtpMulticastGroupInfoEvent) event; + + Log.v("AluRtpFeedback", "mcast=[" + groupInfo.getMulticastGroup() + "], " + + "first burst=[" + groupInfo.getFirstBurstServer() + "], " + + "second burst=[" + groupInfo.getSecondBurstServer() + "], " + + "first retrans=[" + groupInfo.getFirstRetransmissionServer() + "], " + + "second retrans=[" + groupInfo.getSecondRetransmissionServer() + "]"); + } + } + }; + + @Override public void onCreate() { super.onCreate(); @@ -45,6 +95,49 @@ public HttpDataSource.Factory buildHttpDataSourceFactory(DefaultBandwidthMeter b return new DefaultHttpDataSourceFactory(userAgent, bandwidthMeter); } + + public RtpDataSource.Factory buildRtpDataSourceFactory(DefaultBandwidthMeter bandwidthMeter, + String vendor, boolean feedback_events, + String burst_uri, + String retransmission_uri) { + + RtpDataSourceFactory dataSourceFactory = new RtpDataSourceFactory(bandwidthMeter); + + if ((vendor != null) && ("alu".equalsIgnoreCase(vendor))) { + + dataSourceFactory.setFeedbackProperty(RtpDistributionFeedback.Properties.FB_VENDOR, + RtpDistributionFeedback.Providers.ALU); + + if (feedback_events) { + dataSourceFactory.setFeedbackProperty(RtpDistributionFeedback.Properties.FB_EVENTS_CALLBACK, + eventListener); + } + + int flagsScheme = 0; + + if (burst_uri != null) { + dataSourceFactory.setFeedbackProperty(RtpDistributionFeedback.Properties.FB_RAMS_URI, + burst_uri); + + flagsScheme |= RtpDistributionFeedback.Schemes.FB_RAMS; + } + + if (retransmission_uri != null) { + dataSourceFactory.setFeedbackProperty( + RtpDistributionFeedback.Properties.FB_CONGESTION_CONTROL_URI, + retransmission_uri); + + flagsScheme |= RtpDistributionFeedback.Schemes.FB_CONGESTION_CONTROL; + } + + dataSourceFactory.setFeedbackProperty(RtpDistributionFeedback.Properties.FB_SCHEME, + flagsScheme); + + } + + return dataSourceFactory; + } + public boolean useExtensionRenderers() { return BuildConfig.FLAVOR.equals("withExtensions"); } diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index b2750a93bbf..dbbb0236759 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -72,7 +72,10 @@ import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.upstream.HttpDataSource; +import com.google.android.exoplayer2.upstream.RtpDataSource; import com.google.android.exoplayer2.util.Util; +import com.google.android.exoplayer2.util.rtp.RtpExtractorsFactory; + import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.net.CookieHandler; @@ -93,6 +96,10 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi public static final String ACTION_VIEW = "com.google.android.exoplayer.demo.action.VIEW"; public static final String EXTENSION_EXTRA = "extension"; + public static final String VENDOR_EXTRA = "vendor"; + public static final String FEEDBACK_EVENTS_EXTRA = "feedback_events"; + public static final String BURST_URI_EXTRA = "burst_uri"; + public static final String RETRANSMISSION_URI_EXTRA = "retransmission_uri"; public static final String ACTION_VIEW_LIST = "com.google.android.exoplayer.demo.action.VIEW_LIST"; @@ -329,8 +336,13 @@ private void initializePlayer() { } MediaSource[] mediaSources = new MediaSource[uris.length]; for (int i = 0; i < uris.length; i++) { - mediaSources[i] = buildMediaSource(uris[i], extensions[i]); + mediaSources[i] = buildMediaSource(uris[i], extensions[i], + intent.getStringExtra(VENDOR_EXTRA), + intent.getBooleanExtra(FEEDBACK_EVENTS_EXTRA, false), + intent.getStringExtra(BURST_URI_EXTRA), + intent.getStringExtra(RETRANSMISSION_URI_EXTRA)); } + MediaSource mediaSource = mediaSources.length == 1 ? mediaSources[0] : new ConcatenatingMediaSource(mediaSources); String adTagUriString = intent.getStringExtra(AD_TAG_URI_EXTRA); @@ -357,7 +369,9 @@ private void initializePlayer() { updateButtonVisibilities(); } - private MediaSource buildMediaSource(Uri uri, String overrideExtension) { + private MediaSource buildMediaSource(Uri uri, String overrideExtension, String vendor, + boolean feedback_events, String burst_uri, + String retransmission_uri) { int type = TextUtils.isEmpty(overrideExtension) ? Util.inferContentType(uri) : Util.inferContentType("." + overrideExtension); switch (type) { @@ -370,6 +384,13 @@ private MediaSource buildMediaSource(Uri uri, String overrideExtension) { case C.TYPE_HLS: return new HlsMediaSource(uri, mediaDataSourceFactory, mainHandler, eventLogger); case C.TYPE_OTHER: + if (uri.getScheme().equals("rtp")) { + return new ExtractorMediaSource(uri, buildRtpDataSourceFactory(true, vendor, + feedback_events, burst_uri,retransmission_uri), new RtpExtractorsFactory(), + mainHandler, eventLogger); + + } + return new ExtractorMediaSource(uri, mediaDataSourceFactory, new DefaultExtractorsFactory(), mainHandler, eventLogger); default: { @@ -440,6 +461,22 @@ private HttpDataSource.Factory buildHttpDataSourceFactory(boolean useBandwidthMe .buildHttpDataSourceFactory(useBandwidthMeter ? BANDWIDTH_METER : null); } + /** + + * Returns a new RtpDataSource factory. + + * + + * @param useBandwidthMeter Whether to set {@link #BANDWIDTH_METER} as a listener to the new + + * DataSource factory. + + * @param vendor RTP distribution and feedback vendor architecture identifier. + + * @return A new RtpDataSource factory. + + */ + private RtpDataSource.Factory buildRtpDataSourceFactory(boolean useBandwidthMeter, String vendor, + boolean feedback_events, String burst_uri, + String retransmission_uri) { + return ((DemoApplication) getApplication()) + .buildRtpDataSourceFactory(useBandwidthMeter ? BANDWIDTH_METER : null, vendor, + feedback_events, burst_uri, retransmission_uri); + } + /** * Returns an ads media source, reusing the ads loader if one exists. * diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java index c0edb1d1b88..9ec3a63f089 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java @@ -178,6 +178,10 @@ private void readSampleGroup(JsonReader reader, List groups) throws private Sample readEntry(JsonReader reader, boolean insidePlaylist) throws IOException { String sampleName = null; String uri = null; + String vendor = null; + boolean feedback_events = false; + String burst_uri = null; + String retransmission_uri = null; String extension = null; UUID drmUuid = null; String drmLicenseUrl = null; @@ -196,6 +200,18 @@ private Sample readEntry(JsonReader reader, boolean insidePlaylist) throws IOExc case "uri": uri = reader.nextString(); break; + case "vendor": + vendor = reader.nextString(); + break; + case "feedback_events": + feedback_events = "yes".equalsIgnoreCase(reader.nextString()) ? true : false; + break; + case "burst_uri": + burst_uri = reader.nextString(); + break; + case "retransmission_uri": + retransmission_uri = reader.nextString(); + break; case "extension": extension = reader.nextString(); break; @@ -250,7 +266,8 @@ private Sample readEntry(JsonReader reader, boolean insidePlaylist) throws IOExc preferExtensionDecoders, playlistSamplesArray); } else { return new UriSample(sampleName, drmUuid, drmLicenseUrl, drmKeyRequestProperties, - preferExtensionDecoders, uri, extension, adTagUri); + preferExtensionDecoders, uri, vendor, feedback_events, burst_uri, retransmission_uri, + extension, adTagUri); } } @@ -405,14 +422,23 @@ public Intent buildIntent(Context context) { private static final class UriSample extends Sample { public final String uri; + public final String vendor; + public final boolean feedback_events; + public final String burst_uri; + public final String retransmission_uri; public final String extension; public final String adTagUri; public UriSample(String name, UUID drmSchemeUuid, String drmLicenseUrl, String[] drmKeyRequestProperties, boolean preferExtensionDecoders, String uri, - String extension, String adTagUri) { + String vendor, boolean feedback_events, String burst_uri, String retransmission_uri, + String extension, String adTagUri) { super(name, drmSchemeUuid, drmLicenseUrl, drmKeyRequestProperties, preferExtensionDecoders); this.uri = uri; + this.vendor = vendor; + this.feedback_events =feedback_events; + this.burst_uri = burst_uri; + this.retransmission_uri = retransmission_uri; this.extension = extension; this.adTagUri = adTagUri; } @@ -421,6 +447,10 @@ public UriSample(String name, UUID drmSchemeUuid, String drmLicenseUrl, public Intent buildIntent(Context context) { return super.buildIntent(context) .setData(Uri.parse(uri)) + .putExtra(PlayerActivity.VENDOR_EXTRA, vendor) + .putExtra(PlayerActivity.FEEDBACK_EVENTS_EXTRA, feedback_events) + .putExtra(PlayerActivity.BURST_URI_EXTRA, burst_uri) + .putExtra(PlayerActivity.RETRANSMISSION_URI_EXTRA, retransmission_uri) .putExtra(PlayerActivity.EXTENSION_EXTRA, extension) .putExtra(PlayerActivity.AD_TAG_URI_EXTRA, adTagUri) .setAction(PlayerActivity.ACTION_VIEW); diff --git a/library/core/src/main/AndroidManifest.xml b/library/core/src/main/AndroidManifest.xml index 430930a3cad..9278ac11c00 100644 --- a/library/core/src/main/AndroidManifest.xml +++ b/library/core/src/main/AndroidManifest.xml @@ -14,4 +14,4 @@ limitations under the License. --> - + \ No newline at end of file diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/RtpDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/RtpDataSource.java new file mode 100644 index 00000000000..c3aad583c8c --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/RtpDataSource.java @@ -0,0 +1,1101 @@ +/* + * Copyright (C) 2017 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.upstream; + +import android.net.Uri; + +import android.os.ConditionVariable; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Message; +import android.os.Process; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.rtp.DefaultRtpDistributionFeedbackFactory; +import com.google.android.exoplayer2.util.rtp.AluRtpDistributionFeedbackFactory; +import com.google.android.exoplayer2.util.rtp.RtpDistributionFeedback; +import com.google.android.exoplayer2.util.rtp.RtpPacket; +import com.google.android.exoplayer2.util.rtp.RtpPacketQueue; +import com.google.android.exoplayer2.util.rtp.rtcp.RtcpSessionUtils; + +import java.io.IOException; + +/** + * A RTP {@link DataSource}. + */ +public final class RtpDataSource implements DataSource { + + /** + * Thrown when an error is encountered when trying to read from a {@link RtpDataSource}. + */ + public static final class RtpDataSourceException extends IOException { + + public RtpDataSourceException(String message) { + super(message); + } + + public RtpDataSourceException(Exception cause) { + super(cause); + } + + } + + /** + * The maximum transfer unit, in bytes. + */ + public static final int MTU_SIZE = 1500; + + private final TransferListener listener; + + /** + * The RTP distribution and feedback scheme implementation + */ + private RtpDistributionFeedback distributionFeedback; + + /** + * The RTP feedback properties + */ + private final RtpDistributionFeedback.RtpFeedbackProperties feedbackProperties; + + /** + * The RTP source holders + */ + private RtpBurstSourceHolder burstSourceHolder; + private RtpAuthTokenSourceHolder authTokenSourceHolder; + private RtpDistributionSourceHolder distributionSourceHolder; + private RtpRetransmissionSourceHolder retransmissionSourceHolder; + + private long lastTimeStampBytesReaded; + + private DataSpec dataSpec; + private boolean opened; + + /** + * @param listener An optional listener. + */ + public RtpDataSource(TransferListener listener) { + this(listener, new RtpDistributionFeedback.RtpFeedbackProperties()); + } + + /** + * @param listener An optional listener. + * @param feedbackProperties The feedback properties to be set to the data source. + */ + public RtpDataSource(TransferListener listener, + RtpDistributionFeedback.RtpFeedbackProperties feedbackProperties) { + this.listener = listener; + this.feedbackProperties = (feedbackProperties == null) ? + new RtpDistributionFeedback.RtpFeedbackProperties() : feedbackProperties; + } + + /** + * Sets the value of a feedback property. The value will be used to establish a specific a + * feedback scheme and model. + * + * @param property The name of the feedback property. + * @param value The value of the feedback property. + */ + + public void setFeedbackProperty(int property, Object value) { + feedbackProperties.set(property, value); + } + + /** + * Builds specific distribution and feedback implementation from vendor. + * A default distribution and feedback implementation will be created whether no vendor model + * was given from feedback properties. + * + * @return The {@link RtpDistributionFeedback}. + */ + + private RtpDistributionFeedback buildRtpDistributionFeedback() { + if (feedbackProperties.getSnapshot().containsKey(RtpDistributionFeedback.Properties.FB_VENDOR)) { + + int fbVendor = (int) feedbackProperties.getSnapshot().get(RtpDistributionFeedback.Properties. + FB_VENDOR); + + switch (fbVendor) { + case RtpDistributionFeedback.Providers.ALU: + return new AluRtpDistributionFeedbackFactory(RtcpSessionUtils.SSRC(), + RtcpSessionUtils.CNAME()).createDistributionFeedback(); + + default: + return new DefaultRtpDistributionFeedbackFactory(RtcpSessionUtils.SSRC(), + RtcpSessionUtils.CNAME()).createDistributionFeedback(); + } + + } else { + return new DefaultRtpDistributionFeedbackFactory(RtcpSessionUtils.SSRC(), + RtcpSessionUtils.CNAME()).createDistributionFeedback(); + } + } + + @Override + public long open(DataSpec dataSpec) throws RtpDataSourceException { + this.dataSpec = dataSpec; + + distributionFeedback = buildRtpDistributionFeedback(); + distributionSourceHolder = new RtpDistributionSourceHolder(distributionFeedback); + + if (feedbackProperties.getSnapshot().containsKey(RtpDistributionFeedback.Properties. + FB_EVENTS_CALLBACK)) { + + distributionFeedback.setFeedbackEventListener( + (RtpDistributionFeedback.RtpFeedbackEventListener) + feedbackProperties.getSnapshot().get(RtpDistributionFeedback.Properties. + FB_EVENTS_CALLBACK)); + } + + if (feedbackProperties.getSnapshot().containsKey(RtpDistributionFeedback.Properties.FB_SCHEME)) { + + int fbScheme = (int) feedbackProperties.getSnapshot().get(RtpDistributionFeedback. + Properties.FB_SCHEME); + + if ((fbScheme & RtpDistributionFeedback.Schemes.FB_PORT_MAPPING) == + RtpDistributionFeedback.Schemes.FB_PORT_MAPPING) { + + authTokenSourceHolder = new RtpAuthTokenSourceHolder(distributionFeedback); + + if (feedbackProperties.getSnapshot().containsKey(RtpDistributionFeedback.Properties. + FB_PORT_MAPPING_URI)) { + try { + + authTokenSourceHolder.open(Uri.parse((String)feedbackProperties.getSnapshot(). + get(RtpDistributionFeedback.Properties.FB_PORT_MAPPING_URI))); + + } catch (IOException ex) { + // .... + } + } + } + + if ((fbScheme & RtpDistributionFeedback.Schemes.FB_RAMS) == + RtpDistributionFeedback.Schemes.FB_RAMS) { + + burstSourceHolder = new RtpBurstSourceHolder(distributionFeedback); + + if (feedbackProperties.getSnapshot().containsKey(RtpDistributionFeedback.Properties. + FB_RAMS_URI)) { + + try { + + if ((authTokenSourceHolder == null) || (!authTokenSourceHolder.isOpened())) { + burstSourceHolder.open(Uri.parse((String) feedbackProperties.getSnapshot(). + get(RtpDistributionFeedback.Properties.FB_RAMS_URI))); + } + + } catch (IOException ex) { + + try { + + distributionSourceHolder.open(dataSpec.uri); + + } catch (IOException ex2) { + throw new RtpDataSourceException(ex2); + } + + } + } + } + + if ((burstSourceHolder == null) || !burstSourceHolder.isOpened()) { + try { + + if (!distributionSourceHolder.isOpened()) { + distributionSourceHolder.open(dataSpec.uri); + } + + } catch (IOException ex2) { + throw new RtpDataSourceException(ex2); + } + } + + if ((fbScheme & RtpDistributionFeedback.Schemes.FB_CONGESTION_CONTROL) == + RtpDistributionFeedback.Schemes.FB_CONGESTION_CONTROL) { + + retransmissionSourceHolder = new RtpRetransmissionSourceHolder(distributionFeedback); + + if (feedbackProperties.getSnapshot().containsKey(RtpDistributionFeedback.Properties. + FB_CONGESTION_CONTROL_URI)) { + + try { + + retransmissionSourceHolder.open(Uri.parse((String) feedbackProperties.getSnapshot(). + get(RtpDistributionFeedback.Properties.FB_CONGESTION_CONTROL_URI))); + + } catch (IOException ex) { + // Do nothing + } + } + } + + } else { + + try { + + distributionSourceHolder.open(dataSpec.uri); + + } catch (IOException ex) { + throw new RtpDataSourceException(ex); + } + } + + if (listener != null) { + listener.onTransferStart(this, dataSpec); + } + + opened = true; + return C.LENGTH_UNSET; + } + + @Override + public int read(byte[] buffer, int offset, int readLength) throws RtpDataSourceException { + int length = 0; + + if (distributionSourceHolder.isOpened()) { + + if ((burstSourceHolder != null) && + (burstSourceHolder.isOpened() || burstSourceHolder.isDataAvailable())) { + length = burstSourceHolder.read(buffer, offset, readLength); + + } else { + + if ((retransmissionSourceHolder != null)) { + if (retransmissionSourceHolder.isDataAvailable()) { + if (retransmissionSourceHolder.getFirstSequenceNumberAvailable() < + distributionSourceHolder.getFirstSequenceNumberAvailable()) { + + if (retransmissionSourceHolder.getFirstTimeStampAvailable() <= + distributionSourceHolder.getFirstTimeStampAvailable()) { + length = retransmissionSourceHolder.read(buffer, offset, readLength); + + } else { + length = distributionSourceHolder.read(buffer, offset, readLength); + } + + } else if (retransmissionSourceHolder.getFirstTimeStampAvailable() < + distributionSourceHolder.getFirstTimeStampAvailable()) { + length = retransmissionSourceHolder.read(buffer, offset, readLength); + + } else { + length = distributionSourceHolder.read(buffer, offset, readLength); + } + + } else if (retransmissionSourceHolder.isDataPending()) { + long delay = (int) (System.currentTimeMillis() - lastTimeStampBytesReaded); + + if (delay > retransmissionSourceHolder.getMaxDelayTimeForPending()) { + retransmissionSourceHolder.resetAllPacketsRecoveryPending( + distributionSourceHolder.getFirstTimeStampAvailable()); + + length = distributionSourceHolder.read(buffer, offset, readLength); + } + + } else { + length = distributionSourceHolder.read(buffer, offset, readLength); + } + } else { + length = distributionSourceHolder.read(buffer, offset, readLength); + } + } + + } else { + + if (burstSourceHolder != null) { + length = burstSourceHolder.read(buffer, offset, readLength); + } + } + + if (length > 0) { + listener.onBytesTransferred(this, length); + lastTimeStampBytesReaded = System.currentTimeMillis(); + } + + return length; + } + + @Override + public Uri getUri() { + return dataSpec.uri; + } + + @Override + public void close() { + if ((burstSourceHolder != null) && (burstSourceHolder.isOpened())) { + burstSourceHolder.close(); + } + + if ((retransmissionSourceHolder != null) && (retransmissionSourceHolder.isOpened())) { + retransmissionSourceHolder.close(); + } + + if ((distributionSourceHolder != null) && (distributionSourceHolder.isOpened())) { + distributionSourceHolder.close(); + } + + if (opened) { + opened = false; + + if (listener != null) { + listener.onTransferEnd(this); + } + + } + } + + + private class RtpAuthTokenSourceHolder implements + RtpDistributionFeedback.RtpFeedbackTargetSource.AuthTokenEventListener, + Handler.Callback { + + private RtpDistributionFeedback.RtpAuthTokenSource authTokenSource; + + private final Handler mediaHandler; + private final HandlerThread mediaThread; + private final Loader mediaLoader; + + private boolean opened; + private boolean released; + + private static final int MSG_SOURCE_RELEASE = 4; + + public RtpAuthTokenSourceHolder(RtpDistributionFeedback distributionFeedback) { + mediaThread = new HandlerThread("Handler:RtpBurstSource", Process.THREAD_PRIORITY_AUDIO); + mediaThread.start(); + + mediaHandler = new Handler(mediaThread.getLooper(), this); + mediaLoader = new Loader("Loader:RtpBurstSource"); + + opened = false; + released = true; + + try { + + authTokenSource = distributionFeedback.createAuthTokenSource(this); + + } catch (RtpDistributionFeedback.UnsupportedRtpDistributionFeedbackSourceException ex) { + mediaHandler.sendEmptyMessage(MSG_SOURCE_RELEASE); + } + } + + void open(Uri uri) throws IOException { + if (!opened) { + + authTokenSource.open(uri); + + Runnable currentThreadTask = new Runnable() { + @Override + public void run() { + mediaLoader.startLoading(authTokenSource, authTokenSource, 0); + } + }; + + mediaHandler.post(currentThreadTask); + + authTokenSource.sendAuthTokenRequest(); + + opened = true; + released = false; + } + } + + void close() { + if (opened) { + if (!authTokenSource.isLoadCanceled()) { + authTokenSource.cancelLoad(); + authTokenSource.close(); + } + + opened = false; + } + + release(); + } + + void release() { + if (!released) { + Runnable currentThreadTask = new Runnable() { + @Override + public void run() { + mediaLoader.release(); + } + }; + + mediaHandler.post(currentThreadTask); + mediaThread.quit(); + + released = true; + } + } + + public boolean isOpened() { + return opened; + } + + + // RtpDistributionFeedback.RtpFeedbackEventListener.AuthTokenEventListener implementation + @Override + public void onAuthTokenResponse() { + try { + + if (burstSourceHolder != null) { + burstSourceHolder.open(Uri.parse((String) feedbackProperties.getSnapshot(). + get(RtpDistributionFeedback.Properties.FB_RAMS_URI))); + } + + } catch (IOException ex) {} + } + + @Override + public void onAuthTokenResponseBeforeTimeout() { + close(); + } + + @Override + public void onAuthTokenResponseBeforeError() { + close(); + } + + @Override + public void onAuthTokenResponseUnexpected() { + close(); + } + + @Override + public void onRtpAuthTokenSourceError() { + close(); + } + + @Override + public void onRtpAuthTokenSourceCanceled() { + close(); + } + + // Handler.Callback implementation + @Override + public boolean handleMessage(Message msg) { + switch (msg.what) { + + case MSG_SOURCE_RELEASE: { + release(); + return true; + } + + default: + // Should never happen. + throw new IllegalStateException(); + } + } + } + + private class RtpBurstSourceHolder implements + RtpDistributionFeedback.RtpFeedbackTargetSource.BurstEventListener, + Handler.Callback { + + private RtpPacketQueue packetQueue; + private RtpDistributionFeedback.RtpBurstSource burstSource; + + private boolean opened; + private boolean error; + private boolean released; + + private final Handler mediaHandler; + private final HandlerThread mediaThread; + private final Loader mediaLoader; + + private final ConditionVariable loadCondition; + + // Internal messages + private static final int MSG_SOURCE_RELEASE = 1; + + public RtpBurstSourceHolder(RtpDistributionFeedback distributionFeedback) { + + mediaThread = new HandlerThread("Handler:RtpBurstSource", Process.THREAD_PRIORITY_AUDIO); + mediaThread.start(); + + mediaHandler = new Handler(mediaThread.getLooper(), this); + mediaLoader = new Loader("Loader:RtpBurstSource"); + + loadCondition = new ConditionVariable(); + + opened = false; + error = false; + released = true; + + try { + + burstSource = distributionFeedback.createBurstSource(this); + packetQueue = new RtpPacketQueue(burstSource.getMaxBufferCapacity()); + + } catch (RtpDistributionFeedback.UnsupportedRtpDistributionFeedbackSourceException ex) { + mediaHandler.sendEmptyMessage(MSG_SOURCE_RELEASE); + } + } + + void open(Uri uri) throws IOException { + if (!opened) { + + burstSource.open(uri); + + Runnable currentThreadTask = new Runnable() { + @Override + public void run() { + mediaLoader.startLoading(burstSource, burstSource, 0); + } + }; + + mediaHandler.post(currentThreadTask); + + burstSource.sendBurstRapidAcquisitionRequest(dataSpec.uri); + + opened = true; + released = false; + } + } + + public int read(byte[] buffer, int offset, int readLength) throws RtpDataSourceException { + int rbytes; + + if (error && !packetQueue.isDataAvailable()) { + throw new RtpDataSourceException("RtpBurstSource is closed"); + } + + if (!packetQueue.isDataAvailable()) { + loadCondition.block(); + } + + rbytes = packetQueue.get(buffer, offset, readLength); + + loadCondition.close(); + + return rbytes; + } + + void close() { + if (opened) { + if (!burstSource.isLoadCanceled()) { + burstSource.cancelLoad(); + burstSource.close(); + } + + opened = false; + loadCondition.open(); + } + + release(); + } + + void release() { + if (!released) { + Runnable currentThreadTask = new Runnable() { + @Override + public void run() { + mediaLoader.release(); + } + }; + + mediaHandler.post(currentThreadTask); + mediaThread.quit(); + + released = true; + } + } + + boolean isDataAvailable() { + return (packetQueue != null) && (packetQueue.isDataAvailable()); + } + + public boolean isOpened() { + return opened; + } + + public boolean isError() { + return error; + } + + // RtpDistributionFeedback.RtpFeedbackEventListener.BurstEventListener implementation + @Override + public void onBurstRapidAcquisitionAccepted() { + } + + @Override + public void onBurstRapidAcquisitionRejected() { + try { + + burstSource.sendBurstTerminationRequest(); + + } catch (IOException ex) { + // Do nothing + } finally { + close(); + } + } + + @Override + public void oBurstRapidAcquisitionResponseBeforeTimeout() { + close(); + } + + @Override + public void onMulticastJoinSignal() { + try { + + distributionSourceHolder.open(dataSpec.uri); + + } catch (IOException ex) { + // Do nothing + close(); + } + } + + @Override + public void onBurstRapidAcquisitionCompleted() { + try { + + burstSource.sendBurstTerminationRequest(); + + } catch (IOException ex) { + // Do nothing + } finally { + close(); + } + } + + @Override + public void onInvalidToken() { + close(); + } + + @Override + public void onRtpPacketBurstReceived(RtpPacket packet) { + packetQueue.push(packet); + loadCondition.open(); + } + + @Override + public void onRtpBurstSourceError() { + + try { + + if (!distributionSourceHolder.isOpened()) { + distributionSourceHolder.open(dataSpec.uri); + } + + } catch (IOException ex) { + // Do nothing + } finally { + error = true; + close(); + } + } + + @Override + public void onRtpBurstSourceCanceled() { + close(); + } + + + // Handler.Callback implementation + @Override + public boolean handleMessage(Message msg) { + + switch (msg.what) { + + case MSG_SOURCE_RELEASE: { + release(); + return true; + } + + default: + // Should never happen. + throw new IllegalStateException(); + } + } + } + + private class RtpRetransmissionSourceHolder implements + RtpDistributionFeedback.RtpFeedbackTargetSource.RetransmissionEventListener, + Handler.Callback { + + private RtpPacketQueue packetQueue; + private RtpDistributionFeedback.RtpRetransmissionSource retransmissionSource; + + private boolean opened; + private boolean error; + private boolean released; + + private final Handler mediaHandler; + private final HandlerThread mediaThread; + private final Loader mediaLoader; + + private final ConditionVariable loadCondition; + + // Internal messages + private static final int MSG_SOURCE_RELEASE = 1; + + public RtpRetransmissionSourceHolder(RtpDistributionFeedback distributionFeedback) { + + mediaThread = new HandlerThread("Handler:RtpRetransmissionSource", Process.THREAD_PRIORITY_AUDIO); + mediaThread.start(); + + mediaHandler = new Handler(mediaThread.getLooper(), this); + mediaLoader = new Loader("Loader:RtpRetransmissionSource"); + + loadCondition = new ConditionVariable(); + + opened = false; + released = true; + + try { + + retransmissionSource = distributionFeedback.createRetransmissionSource(this); + packetQueue = new RtpPacketQueue(retransmissionSource.getMaxBufferCapacity()); + + } catch (RtpDistributionFeedback.UnsupportedRtpDistributionFeedbackSourceException ex) { + mediaHandler.sendEmptyMessage(MSG_SOURCE_RELEASE); + } + } + + void open(Uri uri) throws IOException { + if (!opened) { + + retransmissionSource.open(uri); + + Runnable currentThreadTask = new Runnable() { + @Override + public void run() { + mediaLoader.startLoading(retransmissionSource, retransmissionSource, 0); + } + }; + + mediaHandler.post(currentThreadTask); + + opened = true; + released = false; + } + } + + public int read(byte[] buffer, int offset, int readLength) throws RtpDataSourceException { + int rbytes; + + if (error && !packetQueue.isDataAvailable()) { + throw new RtpDataSourceException("RtpRetransmissionSource is closed"); + } + + if (!packetQueue.isDataAvailable()) { + loadCondition.block(); + } + + rbytes = packetQueue.get(buffer, offset, readLength); + + loadCondition.close(); + + return rbytes; + } + + void close() { + if (opened) { + if (!retransmissionSource.isLoadCanceled()) { + + try { + + retransmissionSource.sendRetransmissionTerminationRequest(); + + } catch (IOException ex) { } + + retransmissionSource.cancelLoad(); + retransmissionSource.close(); + } + + opened = false; + loadCondition.open(); + } + + release(); + } + + void release() { + if (!released) { + Runnable currentThreadTask = new Runnable() { + @Override + public void run() { + mediaLoader.release(); + } + }; + + mediaHandler.post(currentThreadTask); + mediaThread.quit(); + + released = true; + } + } + + void resetAllPacketsRecoveryPending(long timestamp) { + retransmissionSource.resetAllPacketsRecoveryPending(timestamp); + } + + long getMaxDelayTimeForPending() { + return retransmissionSource.getMaxDelayTimeForPending(); + } + + void lostPacketEvent(int lastSequenceReceived, int numLostPackets) { + if ((retransmissionSource.getPacketsRecoveryPending() + numLostPackets) < + retransmissionSource.getMaxPacketsRecoveryPending()) { + try { + + retransmissionSource.sendRetransmissionPacketRequest(lastSequenceReceived, numLostPackets); + + } catch (IOException ex) { + // Do nothing + } + } else { + retransmissionSource.resetAllPacketsRecoveryPending(0); + } + } + + public boolean isOpened() { + return opened; + } + + public boolean isError() { + return error; + } + + boolean isDataAvailable() { + return (packetQueue != null) && (packetQueue.isDataAvailable()); + } + + int getFirstSequenceNumberAvailable() { + return packetQueue.front().getSequenceNumber(); + } + + long getFirstTimeStampAvailable() { + return packetQueue.front().getTimeStamp(); + } + + public boolean isDataPending() { return retransmissionSource.getPacketsRecoveryPending() > 0; } + + // RtpDistributionFeedback.RtpFeedbackEventListener.RetransmissionEventListener implementation + @Override + public void onInvalidToken() { + close(); + } + + @Override + public void onRtpPacketLossReceived(RtpPacket packet) { + packetQueue.push(packet); + loadCondition.open(); + } + + @Override + public void onRtpRetransmissionSourceError() { + error = true; + close(); + } + + @Override + public void onRtpRetransmissionSourceCanceled() { + close(); + } + + // Handler.Callback implementation + @Override + public boolean handleMessage(Message msg) { + + switch (msg.what) { + + case MSG_SOURCE_RELEASE: { + release(); + return true; + } + + default: + // Should never happen. + throw new IllegalStateException(); + } + } + } + + + private class RtpDistributionSourceHolder implements + RtpDistributionFeedback.RtpDistributionEventListener, + Handler.Callback { + + private RtpPacketQueue packetQueue; + private RtpDistributionFeedback.RtpDistributionSource distributionSource; + + private boolean opened; + private boolean error; + private boolean released; + + private final Handler mediaHandler; + private final HandlerThread mediaThread; + private final Loader mediaLoader; + + private final ConditionVariable loadCondition; + + // Internal messages + private static final int MSG_SOURCE_RELEASE = 1; + + public RtpDistributionSourceHolder(RtpDistributionFeedback distributionFeedback) { + mediaThread = new HandlerThread("Handler:RtpDistributionSource", Process.THREAD_PRIORITY_AUDIO); + mediaThread.start(); + + mediaHandler = new Handler(mediaThread.getLooper(), this); + mediaLoader = new Loader("Loader:RtpDistributionSource"); + + loadCondition = new ConditionVariable(); + + opened = false; + released = true; + + try { + + distributionSource = distributionFeedback.createDistributionSource(this); + packetQueue = new RtpPacketQueue(distributionSource.getMaxBufferCapacity()); + + } catch (RtpDistributionFeedback.UnsupportedRtpDistributionFeedbackSourceException ex) { + mediaHandler.sendEmptyMessage(MSG_SOURCE_RELEASE); + } + } + + void open(Uri uri) throws IOException { + if (!opened) { + distributionSource.open(uri); + + Runnable currentThreadTask = new Runnable() { + @Override + public void run() { + mediaLoader.startLoading(distributionSource, distributionSource, 0); + } + }; + + opened = true; + released = false; + + mediaHandler.post(currentThreadTask); + } + } + + public int read(byte[] buffer, int offset, int readLength) throws RtpDataSourceException { + int rbytes; + + if (!opened || error) { + throw new RtpDataSourceException("RtpDistributionSource is closed"); + } + + if (!packetQueue.isDataAvailable()) { + loadCondition.block(); + } + + rbytes = packetQueue.get(buffer, offset, readLength); + + loadCondition.close(); + + return rbytes; + } + + void close() { + if (opened) { + if (!distributionSource.isLoadCanceled()) { + + distributionSource.cancelLoad(); + distributionSource.close(); + } + + opened = false; + loadCondition.open(); + } + + release(); + } + + void release() { + if (!released) { + Runnable currentThreadTask = new Runnable() { + @Override + public void run() { + mediaLoader.release(); + } + }; + + mediaHandler.post(currentThreadTask); + mediaThread.quit(); + + released = true; + } + } + + int getFirstSequenceNumberAvailable() { + return packetQueue.front().getSequenceNumber(); + } + + long getFirstTimeStampAvailable() { + return packetQueue.front().getTimeStamp(); + } + + public boolean isOpened() { + return opened; + } + + public boolean isError() { + return error; + } + + // RtpDistributionFeedback.RtpDistributionEventListener implementation + @Override + public void onRtpPacketReceived(RtpPacket packet) { + packetQueue.push(packet); + loadCondition.open(); + } + + @Override + public void onRtpLostPacketDetected(int lastSequenceReceived, int numLostPackets) { + if ((retransmissionSourceHolder != null) && retransmissionSourceHolder.isOpened()) { + retransmissionSourceHolder.lostPacketEvent(lastSequenceReceived, numLostPackets); + } + } + + @Override + public void onRtpDistributionSourceError() { + error = true; + close(); + } + + @Override + public void onRtpDistributionSourceCanceled() { + close(); + } + + // Handler.Callback implementation + @Override + public boolean handleMessage(Message msg) { + + switch (msg.what) { + + case MSG_SOURCE_RELEASE: { + release(); + return true; + } + + default: + // Should never happen. + throw new IllegalStateException(); + } + } + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/RtpDataSourceFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/RtpDataSourceFactory.java new file mode 100644 index 00000000000..f969e7974e6 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/RtpDataSourceFactory.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2017 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.upstream; + +import com.google.android.exoplayer2.upstream.DataSource.Factory; +import com.google.android.exoplayer2.util.rtp.RtpDistributionFeedback; + +/** + * A {@link Factory} that produces {@link RtpDataSourceFactory} for RTP data sources. + */ +public final class RtpDataSourceFactory implements Factory { + + private final TransferListener listener; + private final RtpDistributionFeedback.RtpFeedbackProperties feedbackProperties; + public RtpDataSourceFactory() { + this(null); + } + + /** + * @param listener An optional listener. + */ + public RtpDataSourceFactory(TransferListener listener) { + this.listener = listener; + this.feedbackProperties = new RtpDistributionFeedback.RtpFeedbackProperties(); + } + + public final RtpDistributionFeedback.RtpFeedbackProperties getFeedbackProperties() { + return feedbackProperties; + } + + public final void setFeedbackProperty(Integer id, Object value) { + feedbackProperties.set(id, value); + } + + public final void clearFeedbackProperty(Integer id) { + feedbackProperties.remove(id); + } + + @Override + public DataSource createDataSource() { + return new RtpDataSource(listener, feedbackProperties); + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/net/Connectivity.java b/library/core/src/main/java/com/google/android/exoplayer2/util/net/Connectivity.java new file mode 100644 index 00000000000..8a7530a65dc --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/net/Connectivity.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2017 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.util.net; + +import android.app.Application; +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.telephony.TelephonyManager; + +/** + * Check device's network connectivity and speed + * + */ +public class Connectivity { + + private static Application getApplicationContext() throws Exception { + return (Application) Class.forName("android.app.ActivityThread") + .getMethod("currentApplication").invoke(null, (Object[]) null); + } + + /** + * Get the network info + * @return + */ + public static NetworkInfo getNetworkInfo() throws Exception { + ConnectivityManager cm = (ConnectivityManager) getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE); + return cm.getActiveNetworkInfo(); + } + + /** + * Check if there is any connectivity + * @return + */ + public static boolean isConnected() throws Exception { + NetworkInfo info = Connectivity.getNetworkInfo(); + return (info != null && info.isConnected()); + } + + /** + * Check if there is any connectivity to a Wifi network + * @return + */ + public static boolean isConnectedWifi() throws Exception { + NetworkInfo info = Connectivity.getNetworkInfo(); + return (info != null && info.isConnected() && info.getType() == ConnectivityManager.TYPE_WIFI); + } + + /** + * Check if there is any connectivity to a mobile network + * @return + */ + public static boolean isConnectedMobile() throws Exception { + NetworkInfo info = Connectivity.getNetworkInfo(); + return (info != null && info.isConnected() && info.getType() == ConnectivityManager.TYPE_MOBILE); + } + + /** + * Check if there is any connectivity to a mobile network + * @return + */ + public static boolean isConnectedEthernet() throws Exception { + NetworkInfo info = Connectivity.getNetworkInfo(); + return (info != null && info.isConnected() && info.getType() == ConnectivityManager.TYPE_ETHERNET); + } + + /** + * Check if there is fast connectivity + * @return + */ + public static boolean isConnectedFast() throws Exception { + NetworkInfo info = Connectivity.getNetworkInfo(); + return (info != null && info.isConnected() && Connectivity.isConnectionFast(info.getType(),info.getSubtype())); + } + + /** + * Check if the connection is fast + * @return + */ + public static boolean isConnectionFast(int type, int subType){ + if(type==ConnectivityManager.TYPE_WIFI){ + return true; + }else if(type==ConnectivityManager.TYPE_MOBILE){ + switch(subType){ + case TelephonyManager.NETWORK_TYPE_1xRTT: + return false; // ~ 50-100 kbps + case TelephonyManager.NETWORK_TYPE_CDMA: + return false; // ~ 14-64 kbps + case TelephonyManager.NETWORK_TYPE_EDGE: + return false; // ~ 50-100 kbps + case TelephonyManager.NETWORK_TYPE_EVDO_0: + return true; // ~ 400-1000 kbps + case TelephonyManager.NETWORK_TYPE_EVDO_A: + return true; // ~ 600-1400 kbps + case TelephonyManager.NETWORK_TYPE_GPRS: + return false; // ~ 100 kbps + case TelephonyManager.NETWORK_TYPE_HSDPA: + return true; // ~ 2-14 Mbps + case TelephonyManager.NETWORK_TYPE_HSPA: + return true; // ~ 700-1700 kbps + case TelephonyManager.NETWORK_TYPE_HSUPA: + return true; // ~ 1-23 Mbps + case TelephonyManager.NETWORK_TYPE_UMTS: + return true; // ~ 400-7000 kbps + /* + * Above API level 7, make sure to set android:targetSdkVersion + * to appropriate level to use these + */ + case TelephonyManager.NETWORK_TYPE_EHRPD: // API level 11 + return true; // ~ 1-2 Mbps + case TelephonyManager.NETWORK_TYPE_EVDO_B: // API level 9 + return true; // ~ 5 Mbps + case TelephonyManager.NETWORK_TYPE_HSPAP: // API level 13 + return true; // ~ 10-20 Mbps + case TelephonyManager.NETWORK_TYPE_IDEN: // API level 8 + return false; // ~25 kbps + case TelephonyManager.NETWORK_TYPE_LTE: // API level 11 + return true; // ~ 10+ Mbps + // Unknown + case TelephonyManager.NETWORK_TYPE_UNKNOWN: + default: + return false; + } + } else { + return false; + } + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/net/NetworkUtils.java b/library/core/src/main/java/com/google/android/exoplayer2/util/net/NetworkUtils.java new file mode 100644 index 00000000000..4645c2daa53 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/net/NetworkUtils.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2017 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.util.net; + +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; + +public class NetworkUtils { + /** + * Returns MAC address of the given interface name. + * @param interfaceName eth0, wlan0 or NULL=use first interface + * @return mac address or null + */ + public static String getMACAddress(String interfaceName) { + try { + List interfaces = Collections.list(NetworkInterface.getNetworkInterfaces()); + + for (NetworkInterface intf : interfaces) { + if (interfaceName != null) { + if (!intf.getName().equalsIgnoreCase(interfaceName)) continue; + } + + byte[] mac = intf.getHardwareAddress(); + + if (mac==null) return ""; + StringBuilder buf = new StringBuilder(); + + for (int idx=0; idx0) buf.deleteCharAt(buf.length()-1); + + return buf.toString(); + } + + } catch (Exception ex) { } + + return null; + } + + /** + * Returns local address. + * @return ip address or null + */ + public static String getLocalAddress() { + try { + + for (Enumeration networkEnum = NetworkInterface.getNetworkInterfaces(); networkEnum.hasMoreElements();) { + + NetworkInterface network = networkEnum.nextElement(); + + for (Enumeration enumIpAddr = network.getInetAddresses(); enumIpAddr.hasMoreElements();) { + InetAddress inetAddress = enumIpAddr.nextElement(); + + if (!inetAddress.isLoopbackAddress() && inetAddress instanceof Inet4Address) { + return inetAddress.getHostAddress().toString(); + } + } + } + + } catch (Exception ex) { } + + return null; + } +} \ No newline at end of file diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/rtp/AluRtpDistributionFeedback.java b/library/core/src/main/java/com/google/android/exoplayer2/util/rtp/AluRtpDistributionFeedback.java new file mode 100644 index 00000000000..5796a07da9e --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/rtp/AluRtpDistributionFeedback.java @@ -0,0 +1,852 @@ +/* + * Copyright (C) 2017 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.util.rtp; + +import android.net.Uri; +import android.util.SparseArray; + +import com.google.android.exoplayer2.util.net.NetworkUtils; +import com.google.android.exoplayer2.util.rtp.rtcp.RtcpFeedbackPacket; +import com.google.android.exoplayer2.util.rtp.rtcp.RtcpPacketBuilder; +import com.google.android.exoplayer2.util.rtp.rtcp.RtcpPacketUtils; +import com.google.android.exoplayer2.util.rtp.rtcp.RtcpTokenPacket; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + + +/** + * The RTP Distribution and Feedback Model implementation based on Alcate-Lucent architecture + */ +public final class AluRtpDistributionFeedback implements RtpDistributionFeedback { + + private final long ssrc; + private final String cname; + + private long ssrcSender; + private boolean ssrcSenderReceived; + + private int firstAudioSequence; + private int firstVideoSequence; + + private int lastSequenceReceived; + + private boolean multicastSwitched; + + // Default socket time-out in milliseconds + private static final int BURST_SOURCE_TIMEOUT = 50; + private static final int DISTRIBUTION_SOURCE_TIMEOUT = 2000; + + private final AluRtpHeaderExtensionParser rtpHeadExtParser; + private RtpFeedbackEventListener feedbackListener; + + public AluRtpDistributionFeedback(long ssrc, String cname) { + this.ssrc = ssrc; + this.cname = cname; + + ssrcSender = 0L; + + multicastSwitched = false; + ssrcSenderReceived = false; + + firstAudioSequence = UNKNOWN_SEQ; + firstVideoSequence = UNKNOWN_SEQ; + + lastSequenceReceived = UNKNOWN_SEQ; + + rtpHeadExtParser = new AluRtpHeaderExtensionParser(); + } + + @Override + public RtpAuthTokenSource createAuthTokenSource( + RtpFeedbackTargetSource.AuthTokenEventListener eventListener) + throws UnsupportedRtpDistributionFeedbackSourceException { + throw new UnsupportedRtpDistributionFeedbackSourceException( + "Authentication Token Source unsupported"); + } + + @Override + public RtpBurstSource createBurstSource( + RtpFeedbackTargetSource.BurstEventListener eventListener) + throws UnsupportedRtpDistributionFeedbackSourceException { + return new AluRtpBurstSource(BURST_SOURCE_TIMEOUT, eventListener); + } + + @Override + public RtpRetransmissionSource createRetransmissionSource( + RtpFeedbackTargetSource.RetransmissionEventListener eventListener) + throws UnsupportedRtpDistributionFeedbackSourceException { + return new AluRtpRetransmissionSource(eventListener); + } + + @Override + public RtpDistributionSource createDistributionSource( + RtpDistributionEventListener eventListener) + throws UnsupportedRtpDistributionFeedbackSourceException { + return new AluRtpDistributionSource(DISTRIBUTION_SOURCE_TIMEOUT, eventListener); + } + + @Override + public void setFeedbackEventListener( + RtpFeedbackEventListener feedbackListener) { + this.feedbackListener = feedbackListener; + } + + private static final boolean isAudioPacket(RtpPacket packet) { + return ((packet.getHeaderExtension()[5] & 0x3f) >> 4) == 1; + } + + private static final boolean isMulticastJoinSignal(RtpPacket packet) { + return ((packet.getHeaderExtension()[5] & 0x0f) >> 3) == 1; + } + + private static final boolean isAluExtension(RtpPacket packet) { + return (((packet.getHeaderExtension()[0] & 0xff) == 0xbe) && + ((packet.getHeaderExtension()[1] & 0xff) == 0xde)); + } + + + private final class AluRtpBurstSource extends RtpBurstSource { + // The maximum buffer capacity, in packets. + private static final int MAX_BUFFER_CAPACITY = 1024; + + private boolean audioSynch; + private boolean videoSynch; + + private boolean multicastJoinSignal; + + private final RtpFeedbackTargetSource.BurstEventListener eventListener; + + public AluRtpBurstSource(int socketTimeoutMillis, + RtpFeedbackTargetSource.BurstEventListener eventListener) { + super(socketTimeoutMillis, eventListener); + + this.eventListener = eventListener; + + audioSynch = false; + videoSynch = false; + + multicastJoinSignal = false; + } + + @Override + public int getMaxBufferCapacity() { + return MAX_BUFFER_CAPACITY; + } + + @Override + boolean isRapidAcquisitionResponse(RtcpFeedbackPacket packet) { + // Not supported + return true; + } + + @Override + boolean isRapidAcquisitionAccepted(RtcpFeedbackPacket packet) { + // Not supported + return true; + } + + @Override + boolean isAuthTokenRejected(RtcpTokenPacket packet) { + // Not supported + return false; + } + + @Override + protected boolean processRtpPacket(RtpPacket packet) { + + if (!multicastJoinSignal && isMulticastJoinSignal(packet)) { + eventListener.onMulticastJoinSignal(); + multicastJoinSignal = true; + } + + if (!ssrcSenderReceived) { + ssrcSender = packet.getSsrc(); + ssrcSenderReceived = true; + } + + if (isAudioPacket(packet)) { + + if (audioSynch) + return false; + + if (firstAudioSequence == packet.getSequenceNumber()) { + audioSynch = true; + + if (videoSynch) { + multicastSwitched = true; + eventListener.onBurstRapidAcquisitionCompleted(); + } + + return false; + } + else if (videoSynch) { + audioSynch = true; + multicastSwitched = true; + eventListener.onBurstRapidAcquisitionCompleted(); + return false; + } + } + else { + + if (videoSynch) + return false; + + if (firstVideoSequence == packet.getSequenceNumber()) { + videoSynch = true; + + if (audioSynch) { + multicastSwitched = true; + eventListener.onBurstRapidAcquisitionCompleted(); + } + + return false; + } + } + + return true; + } + + // BurstMessages Implementation + @Override + public void sendBurstRapidAcquisitionRequest(Uri uri) throws IOException { + byte fccr_pkt[] = new byte [0]; + InetAddress srcAddr, hostAddr; + + try { + + srcAddr = InetAddress.getByName(uri.getHost()); + hostAddr = InetAddress.getByName(NetworkUtils.getLocalAddress()); + + } catch (UnknownHostException ex) { + throw new IOException(ex); + } + + byte[] start = RtcpPacketUtils.longToBytes((long)300, 2); + byte[] sAddr = srcAddr.getAddress(); + byte[] sPort = RtcpPacketUtils.longToBytes((long)uri.getPort(), 2); + byte[] hAddr = hostAddr.getAddress(); + byte[] hPort = RtcpPacketUtils.longToBytes((long)0, 2); + + fccr_pkt = RtcpPacketUtils.append(fccr_pkt, start); + + fccr_pkt = RtcpPacketUtils.append(fccr_pkt, sPort); + fccr_pkt = RtcpPacketUtils.append(fccr_pkt, sAddr); + + byte[] bounded = new byte [2]; + fccr_pkt = RtcpPacketUtils.append (fccr_pkt, bounded); + fccr_pkt = RtcpPacketUtils.append(fccr_pkt, RtcpPacketUtils.swapBytes(hPort)); + fccr_pkt = RtcpPacketUtils.append(fccr_pkt, RtcpPacketUtils.swapBytes(hAddr)); + + byte[] bytes = RtcpPacketBuilder.buildAppPacket(ssrc, cname, + "FCCR", fccr_pkt); + + sendMessageFromBytes(bytes, bytes.length); + + eventListener.onBurstRapidAcquisitionAccepted(); + } + + @Override + public void sendBurstTerminationRequest() throws IOException { + byte[] bytes = RtcpPacketBuilder.buildByePacket(ssrc, cname); + sendMessageFromBytes(bytes, bytes.length); + } + } + + private final class AluRtpRetransmissionSource extends RtpRetransmissionSource { + // The maximum buffer capacity, in packets. + private static final int MAX_BUFFER_CAPACITY = 512; + + private static final int PACKET_LOSS_CAPACITY = 512; + private static final double PACKET_LOSS_PERCENT = 0.7; + + // The maximum timeout delay for packet pending + private static final int MAX_TIMEOUT_DELAY = 1000; + + private static final double PACKET_LOSS_ACCEPTABLE = PACKET_LOSS_CAPACITY * PACKET_LOSS_PERCENT; + + // The current number of lost packet pending to recovery + private int lostPacketPending; + + private final LinkedList keys; + private final SparseArray timestamps; + + public AluRtpRetransmissionSource(RtpFeedbackTargetSource.RetransmissionEventListener + eventListener) { + super(eventListener); + + timestamps = new SparseArray(PACKET_LOSS_CAPACITY); + keys = new LinkedList<>(); + + lostPacketPending = 0; + } + + @Override + public int getMaxBufferCapacity() { + return MAX_BUFFER_CAPACITY; + } + + @Override + boolean isAuthTokenRejected(RtcpTokenPacket packet) { + return false; + } + + @Override + protected boolean processRtpPacket(RtpPacket packet) { + Long timestamp = timestamps.get(packet.getSequenceNumber()); + + if (timestamp != null) { + packet.setTimestamp(timestamp); + timestamps.remove(packet.getSequenceNumber()); + keys.remove((Integer)packet.getSequenceNumber()); + + lostPacketPending--; + + return true; + } + + return false; + } + + @Override + public void resetAllPacketsRecoveryPending(long timestamp) { + if (timestamp <= 0) { + keys.clear(); + timestamps.clear(); + lostPacketPending = 0; + } + else { + for (int index=0; index < keys.size(); index++) { + int seq = keys.get(index); + + if (timestamps.get(seq) <= timestamp) { + keys.remove(index); + timestamps.remove(seq); + + lostPacketPending--; + } + else { + break; + } + } + } + } + + @Override + public long getMaxDelayTimeForPending() { + return MAX_TIMEOUT_DELAY; + } + + @Override + public int getPacketsRecoveryPending() { + return lostPacketPending; + } + + @Override + public int getMaxPacketsRecoveryPending() { + return (int) PACKET_LOSS_ACCEPTABLE; + } + + // RetransmissionMessages implementation + @Override + public void sendRetransmissionPacketRequest(int lastSequenceReceived, int numLostPackets) + throws IOException { + + List fbInformation = new ArrayList(); + + int bitmaskNextSequences, bitmaskShift; + int firstSequence, numPackets = 0; + + long currentTime = System.currentTimeMillis(); + + while (numLostPackets > 0) { + numPackets++; + + firstSequence = ((lastSequenceReceived + numPackets) < MAX_PACKET_SEQ) ? + (lastSequenceReceived + numPackets) : + ((lastSequenceReceived + numPackets) - MAX_PACKET_SEQ); + + --numLostPackets; + + timestamps.put(firstSequence, currentTime); + keys.add(firstSequence); + + for (bitmaskShift = 0, bitmaskNextSequences = 0; + (bitmaskShift < BITMASK_LENGTH) && (numLostPackets > 0); + ++bitmaskShift, ++numPackets, --numLostPackets) { + + bitmaskNextSequences |= ((0xffff) & (1 << bitmaskShift)); + + int sequence = ((firstSequence + bitmaskShift + 1) < MAX_PACKET_SEQ) ? + (firstSequence + bitmaskShift + 1) : + ((firstSequence + bitmaskShift + 1) - MAX_PACKET_SEQ); + + timestamps.put(sequence, currentTime); + keys.add(sequence); + } + + fbInformation.add( + new RtcpPacketBuilder.NackFbElement(firstSequence, bitmaskNextSequences)); + } + + if (fbInformation.size() > 0) { + byte[] bytes = RtcpPacketBuilder.buildNackPacket(ssrc, cname, + ssrcSender, fbInformation); + + sendMessageFromBytes(bytes, bytes.length); + lostPacketPending += numPackets; + } + } + + @Override + public void sendRetransmissionTerminationRequest() throws IOException { + byte[] bytes = RtcpPacketBuilder.buildByePacket(ssrc, cname); + + sendMessageFromBytes(bytes, bytes.length); + lostPacketPending = 0; + } + } + + private final class AluRtpDistributionSource extends RtpDistributionSource { + // The maximum buffer capacity, in packets. + private static final int MAX_BUFFER_CAPACITY = 2048; + + private final RtpDistributionEventListener eventListener; + + public AluRtpDistributionSource(int socketTimeoutMillis, + RtpDistributionEventListener eventListener) { + super(socketTimeoutMillis, eventListener); + + this.eventListener = eventListener; + } + + @Override + public int getMaxBufferCapacity() { + return MAX_BUFFER_CAPACITY; + } + + @Override + protected boolean processRtpPacket(RtpPacket packet) { + if ((feedbackListener != null) && packet.isExtension() && isAluExtension(packet)) { + if (!rtpHeadExtParser.isLoaded()) { + rtpHeadExtParser.parseHeader(packet.getHeaderExtension()); + } + } + + if (!ssrcSenderReceived) { + ssrcSender = packet.getSsrc(); + ssrcSenderReceived = true; + } + + if (lastSequenceReceived != UNKNOWN_SEQ) { + int nextSequence = ((lastSequenceReceived + 1) < MAX_PACKET_SEQ) ? + (lastSequenceReceived + 1) : (lastSequenceReceived + 1) - MAX_PACKET_SEQ; + + if (nextSequence != packet.getSequenceNumber()) { + int numLostPackets = (packet.getSequenceNumber() > lastSequenceReceived) ? + (packet.getSequenceNumber() - lastSequenceReceived) : + (MAX_PACKET_SEQ - lastSequenceReceived) + packet.getSequenceNumber() + 1; + + eventListener.onRtpLostPacketDetected(lastSequenceReceived, numLostPackets); + } + } + + if (!multicastSwitched) { + if (isAudioPacket(packet)) { + if (firstAudioSequence == UNKNOWN_SEQ) { + firstAudioSequence = packet.getSequenceNumber(); + } + } else { + if (firstVideoSequence == UNKNOWN_SEQ) { + firstVideoSequence = packet.getSequenceNumber(); + } + } + } + + packet.setTimestamp(System.currentTimeMillis()); + + lastSequenceReceived = packet.getSequenceNumber(); + + return true; + } + } + + /* + Alcatel-Lucent RTP Header Extension Format + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | 0xbede | length=in 32 bits words | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | ID=1 | len=3 |B|E| ST|S|r|PRI| FPRI|r| GOP end countdown | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | ID=4 | len=6 | ??? - payload row index | | + +-+-+-+-+-+-+-+-+-------------------------------+ | + | ALU/Alu RTP ext type 4 payload row | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | ID=5 | len=6 | | + +-+-+-+-+-+-+-+-+ ALU/Alu RTP ext type 5 payload | + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | ID=7 | len=2 | TDEC_90kHz (signed - 90KHz units) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + */ + + private final class AluRtpHeaderExtensionParser { + private final static int INFO_SIZE = 32; + + private int skip; + private int state; + + private boolean begin; + private boolean loaded; + + private final int NONE_SEEK = 0; + private final int TYPE_SEEK = 1; + private final int SIZE_SEEK = 2; + private final int CHECKSUM_SEEK = 3; + private final int INFO_SEEK = 4; + + private final byte[] info; + + public AluRtpHeaderExtensionParser() { + info = new byte[INFO_SIZE]; + begin = loaded = false; + reset(); + } + + public boolean isLoaded() { + return loaded; + } + + private void reset() { + skip = 0; + state = NONE_SEEK; + } + + private String toIpAddress(byte[] bytes, int offset) { + return (((bytes[offset] & 0xf0) >> 4) * 16 + (bytes[offset] & 0x0f)) + "." + + (((bytes[offset+1] & 0xf0) >> 4) * 16 + (bytes[offset+1] & 0x0f)) + "." + + (((bytes[offset+2] & 0xf0) >> 4) * 16 + (bytes[offset+2] & 0x0f)) + "." + + (((bytes[offset+3] & 0xf0) >> 4) * 16 + (bytes[offset+3] & 0x0f)); + } + + private int toPortNumber(byte[] bytes, int offset) { + return (bytes[offset] & 0xff) * 256 + bytes[offset+1]; + } + + private void parseId5(byte[] bytes, int offset) { + int subtype = bytes[offset] & 0x0f; + + if (begin) { + + String ipAddr = toIpAddress(bytes, offset + 1); + int port = toPortNumber(bytes, offset + 5); + + if (subtype == 1) { + feedbackListener.onRtpFeedbackEvent( + new AluDefaultRtpRetransmissionServerEvent(ipAddr + ":" + port)); + + } else if (subtype == 3) { + feedbackListener.onRtpFeedbackEvent( + new AluDefaultRtpBurstServerEvent(ipAddr + ":" + port)); + } + } + } + + private void parseInfo() { + String mcastIpAddr = toIpAddress(info, 0); + int mcastPort = toPortNumber(info, 4); + + String firstBurstIpAddr = toIpAddress(info, 6); + int firstBurstPort = toPortNumber(info, 10); + + String secondBurstIpAddr = toIpAddress(info, 12); + int secondBurstPort = toPortNumber(info, 16); + + String firstRetransIpAddr = toIpAddress(info, 18); + int firstRetransPort = toPortNumber(info, 22); + + String secondRetransIpAddr = toIpAddress(info, 24); + int secondRetransPort = toPortNumber(info, 28); + + feedbackListener.onRtpFeedbackEvent( + new AluRtpMulticastGroupInfoEvent(mcastIpAddr + ":" + mcastPort, + firstBurstIpAddr + ":" + firstBurstPort, + secondBurstIpAddr + ":" + secondBurstPort, + firstRetransIpAddr + ":" + firstRetransPort, + secondRetransIpAddr + ":" + secondRetransPort)); + } + + private void parseId4(byte[] bytes, int offset, int length) { + int subtype = (bytes[offset] & 0xf0) >> 4; + int index = ((bytes[offset] & 0x0f) * 256) + (((bytes[offset+1] & 0xf0) >> 4) * 16) + + (bytes[offset+1] & 0x0f); + + int seek = offset + 2; // (skip subtype and row index bytes: 2 + int total = offset + length; + + if (subtype == 1) { + + if (index == 0) { + + if (begin) { + + loaded = true; + feedbackListener.onRtpFeedbackEvent( + new AluRtpFeedbackConfigDiscoveryEnded()); + + return; + } + + begin = true; + feedbackListener.onRtpFeedbackEvent( + new AluRtpFeedbackConfigDiscoveryStarted()); + } + + if (begin) { + + while (seek < total) { + + switch (state) { + + case NONE_SEEK: { + if ((bytes[seek] & 0xff) == 0x02) { + state = TYPE_SEEK; + } + + seek++; + } + + break; + + case TYPE_SEEK: { + if ((bytes[seek] & 0xff) == 0x02) { + state = SIZE_SEEK; + skip = 2; + } else { + reset(); + } + + seek++; + } + + break; + + case SIZE_SEEK: { + switch (skip) { + case 2: { + if ((bytes[seek] & 0xff) == 0x00) { + skip--; + seek++; + } else { + state = TYPE_SEEK; + } + } + + break; + + case 1: { + if ((bytes[seek] & 0xff) == 0x20) { + state = CHECKSUM_SEEK; + skip = 2; + seek++; + } else { + reset(); + } + } + + break; + + default: { + reset(); + } + + break; + } + } + + break; + + case CHECKSUM_SEEK: { + switch (skip) { + case 2: { + skip--; + seek++; + } + + break; + + case 1: { + state = INFO_SEEK; + skip = 0; + seek++; + } + + break; + + default: { + reset(); + } + + break; + } + } + + break; + + case INFO_SEEK: { + if (skip + 1 < INFO_SIZE) { + info[skip++] = bytes[seek++]; + + } else { + info[skip++] = bytes[seek++]; + parseInfo(); + reset(); + return; + } + } + + break; + } + } + } + } + } + + public boolean parseHeader(byte[] header) { + int seek=4; + + while ((seek < header.length) && !loaded) { + int id = (header[seek] >> 4) & 0x0f; + int length = header[seek] & 0x0f; + + seek++; + + switch (id) { + case 1: { + seek+=length; + } + + break; + + case 4: { + parseId4(header, seek, length + 1); + seek+=length+1; + } + + break; + + case 5: { + parseId5(header, seek); + seek+=length+1; + } + + break; + + default: { + seek+=length+1; + } + + break; + } + } + + return true; + } + } + + public final class AluRtpFeedbackConfigDiscoveryStarted implements RtpFeedbackEvent { + } + + public final class AluRtpFeedbackConfigDiscoveryEnded implements RtpFeedbackEvent { + } + + public final class AluDefaultRtpBurstServerEvent implements RtpFeedbackEvent { + + private final String burstServer; + + public AluDefaultRtpBurstServerEvent(String burstServer) { + this.burstServer = burstServer; + } + + public String getBurstServer() { + return burstServer; + } + } + + public final class AluDefaultRtpRetransmissionServerEvent implements RtpFeedbackEvent { + + private final String retransmissionServer; + + public AluDefaultRtpRetransmissionServerEvent(String retransmissionServer) { + this.retransmissionServer = retransmissionServer; + } + + public String getRetransmissionServer() { + return retransmissionServer; + } + } + + public final class AluRtpMulticastGroupInfoEvent implements RtpFeedbackEvent { + + private final String multicastGroup; + + private final String firstBurstServer; + private final String secondBurstServer; + + private final String firstRetransmissionServer; + private final String secondRetransmissionServer; + + public AluRtpMulticastGroupInfoEvent(String multicastGroup, + String firstBurstServer, String secondBurstServer, + String firstRetransmissionServer, + String secondRetransmissionServer) + { + this.multicastGroup = multicastGroup; + this.firstBurstServer = firstBurstServer; + this.secondBurstServer = secondBurstServer; + this.firstRetransmissionServer = firstRetransmissionServer; + this.secondRetransmissionServer = secondRetransmissionServer; + } + + public String getMulticastGroup() { + return multicastGroup; + } + + public String getFirstBurstServer() { + return firstBurstServer; + } + + public String getSecondBurstServer() { + return secondBurstServer; + } + + public String getFirstRetransmissionServer() { + return firstRetransmissionServer; + } + + public String getSecondRetransmissionServer() { + return secondRetransmissionServer; + } + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/rtp/AluRtpDistributionFeedbackFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/util/rtp/AluRtpDistributionFeedbackFactory.java new file mode 100644 index 00000000000..d2ea6e25ff6 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/rtp/AluRtpDistributionFeedbackFactory.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2017 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.util.rtp; + +public class AluRtpDistributionFeedbackFactory implements RtpDistributionFeedback.Factory { + + private final long ssrc; + private final String cname; + + public AluRtpDistributionFeedbackFactory(long ssrc, String cname) { + this.ssrc = ssrc; + this.cname = cname; + } + + @Override + public RtpDistributionFeedback createDistributionFeedback() { + return new AluRtpDistributionFeedback(ssrc, cname); + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/rtp/DefaultRtpDistributionFeedback.java b/library/core/src/main/java/com/google/android/exoplayer2/util/rtp/DefaultRtpDistributionFeedback.java new file mode 100644 index 00000000000..b82ead96be4 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/rtp/DefaultRtpDistributionFeedback.java @@ -0,0 +1,377 @@ +/* + * Copyright (C) 2017 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.util.rtp; + +import android.net.Uri; + +import com.google.android.exoplayer2.upstream.Loader; +import com.google.android.exoplayer2.util.rtp.rtcp.RtcpFeedbackPacket; +import com.google.android.exoplayer2.util.rtp.rtcp.RtcpPacketBuilder; +import com.google.android.exoplayer2.util.rtp.rtcp.RtcpTokenPacket; + +import java.io.IOException; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.List; + + +/** + * The Default RTP Distribution and Feedback Model implementation based on IETF Standards + */ +public final class DefaultRtpDistributionFeedback implements RtpDistributionFeedback { + + private final long ssrc; + private final String cname; + + private long ssrcSender; + private boolean ssrcSenderReceived; + + // Default socket time-out in milliseconds + private static final int BURST_SOURCE_TIMEOUT = 50; + private static final int DISTRIBUTION_SOURCE_TIMEOUT = 2000; + + public DefaultRtpDistributionFeedback(long ssrc, String cname) { + this.ssrc = ssrc; + this.cname = cname; + } + + @Override + public RtpAuthTokenSource createAuthTokenSource( + RtpFeedbackTargetSource.AuthTokenEventListener eventListener) + throws UnsupportedRtpDistributionFeedbackSourceException { + return new DefaultRtpAuthTokenSource(0, eventListener); + } + + @Override + public RtpBurstSource createBurstSource( + RtpFeedbackTargetSource.BurstEventListener eventListener) + throws UnsupportedRtpDistributionFeedbackSourceException { + return new DefaultRtpBurstSource(BURST_SOURCE_TIMEOUT, eventListener); + } + + @Override + public RtpRetransmissionSource createRetransmissionSource( + RtpFeedbackTargetSource.RetransmissionEventListener eventListener) + throws UnsupportedRtpDistributionFeedbackSourceException { + return new DefaultRtpRetransmissionSource(eventListener); + } + + @Override + public RtpDistributionSource createDistributionSource( + RtpDistributionEventListener eventListener) + throws UnsupportedRtpDistributionFeedbackSourceException { + return new DefaultRtpDistributionSource(DISTRIBUTION_SOURCE_TIMEOUT, eventListener); + } + + @Override + public void setFeedbackEventListener( + RtpFeedbackEventListener feedbackListener) { + // TODO default implementation + } + + private final class DefaultRtpAuthTokenSource extends RtpAuthTokenSource { + private final byte[] nonce; + + public DefaultRtpAuthTokenSource(int socketTimeoutMillis, + RtpFeedbackTargetSource.AuthTokenEventListener + eventListener) { + super(socketTimeoutMillis, eventListener); + + nonce = new byte[32]; + new SecureRandom().nextBytes(nonce); + } + + @Override + protected byte[] getRandomNonce() { + return nonce; + } + + // AuthTokenMessages standard implementation (IETF RFC 6284) + @Override + public synchronized void sendAuthTokenRequest() throws IOException { + byte[] bytes = RtcpPacketBuilder.buildPortMappingRequestPacket(ssrc, cname, + getRandomNonce()); + + sendMessageFromBytes(bytes, bytes.length); + + setAuthTokenResponsePending(true); + } + } + + private final class DefaultRtpBurstSource extends RtpBurstSource { + + // The maximum buffer capacity, in packets. + private static final int MAX_BUFFER_CAPACITY = 1024; + + public DefaultRtpBurstSource(int socketTimeoutMillis, + RtpFeedbackTargetSource.BurstEventListener eventListener) { + super(socketTimeoutMillis, eventListener); + } + + @Override + public int getMaxBufferCapacity() { + return MAX_BUFFER_CAPACITY; + } + + @Override + protected boolean isRapidAcquisitionResponse(RtcpFeedbackPacket packet) { + // Rapid Acquisition response based in standard implementation (IETF RFC 6285) + if ((packet.getPayloadType() == RtcpPacketBuilder.RTCP_RTPFB) + && (packet.getFmt() == RtcpPacketBuilder.RTCP_SFMT_RAMS_INFO)) { + return true; + } + + return false; + } + + @Override + protected boolean isRapidAcquisitionAccepted(RtcpFeedbackPacket packet) { + // TODO payload decoding based in standard implementation (IETF RFC 6285) + // ... + // decode the feedback control information (from payload) and + // evaluate the previous request + + return true; + } + + @Override + protected boolean isAuthTokenRejected(RtcpTokenPacket packet) { + if ((packet.getPayloadType() == RtcpPacketBuilder.RTCP_TOKEN) + && (packet.getSmt() == RtcpPacketBuilder.RTCP_SMT_TOKEN_VERIFY_FAIL)) { + + return true; + } + + return false; + } + + @Override + protected boolean processRtpPacket(RtpPacket packet) { + // TODO + + if (!ssrcSenderReceived) { + ssrcSender = packet.getSsrc(); + ssrcSenderReceived = true; + } + + return true; + } + + // BurstMessages standard implementation (IETF RFC 6285) + @Override + public void sendBurstRapidAcquisitionRequest(Uri uri) throws IOException { + byte[] bytes = RtcpPacketBuilder.buildRamsRequestPacket(ssrc, cname, + ssrcSender, new ArrayList(), + new ArrayList()); + + sendMessageFromBytes(bytes, bytes.length); + } + + @Override + public void sendBurstTerminationRequest() throws IOException { + byte[] bytes = RtcpPacketBuilder.buildRamsTerminationPacket(ssrc, cname, + ssrcSender, new ArrayList(), + new ArrayList()); + + sendMessageFromBytes(bytes, bytes.length); + } + } + + + private final class DefaultRtpRetransmissionSource extends RtpRetransmissionSource { + + // The maximum buffer capacity, in packets. + private static final int MAX_BUFFER_CAPACITY = 512; + + private final RtpFeedbackTargetSource.RetransmissionEventListener eventListener; + + public DefaultRtpRetransmissionSource(RtpFeedbackTargetSource.RetransmissionEventListener + eventListener) { + super(eventListener); + + this.eventListener = eventListener; + } + + @Override + public int getMaxBufferCapacity() { + return MAX_BUFFER_CAPACITY; + } + + @Override + public void resetAllPacketsRecoveryPending(long timestamp) { + + } + + @Override + public int getPacketsRecoveryPending() { + return 0; + } + + @Override + public int getMaxPacketsRecoveryPending() { + return 0; + } + + @Override + protected boolean processRtpPacket(RtpPacket packet) { + // TODO + return true; + } + + @Override + public long getMaxDelayTimeForPending() { + return 0; + } + + // RetransmissionMessages implementation (IETF RFC 5760, IETF RFC 4585, IETF RFC 1889) + @Override + public void sendRetransmissionPacketRequest(int lastSequenceReceived, int numLostPackets) + throws IOException { + // TODO add token into Nack packet when an authentication token is used + List fbInformation = new ArrayList(); + + int bitmaskNextSequences, bitmaskShift; + int firstSequence, numPackets = 0; + + long currentTime = System.currentTimeMillis(); + + while (numLostPackets > 0) { + numPackets++; + + firstSequence = ((lastSequenceReceived + numPackets) < MAX_PACKET_SEQ) ? + (lastSequenceReceived + numPackets) : + ((lastSequenceReceived + numPackets) - MAX_PACKET_SEQ); + + --numLostPackets; + + for (bitmaskShift = 0, bitmaskNextSequences = 0; + (bitmaskShift < BITMASK_LENGTH) && (numLostPackets > 0); + ++bitmaskShift, ++numPackets, --numLostPackets) { + + bitmaskNextSequences |= ((0xffff) & (1 << bitmaskShift)); + + int sequence = ((firstSequence + bitmaskShift + 1) < MAX_PACKET_SEQ) ? + (firstSequence + bitmaskShift + 1) : + ((firstSequence + bitmaskShift + 1) - MAX_PACKET_SEQ); + } + + fbInformation.add( + new RtcpPacketBuilder.NackFbElement(firstSequence, bitmaskNextSequences)); + } + + if (fbInformation.size() > 0) { + byte[] bytes = RtcpPacketBuilder.buildNackPacket(ssrc, cname, + ssrcSender, fbInformation); + + sendMessageFromBytes(bytes, bytes.length); + } + } + + @Override + public void sendRetransmissionTerminationRequest() throws IOException { + // TODO add token into Nack packet when an authentication token is used + byte[] bytes = RtcpPacketBuilder.buildByePacket(ssrc, cname); + + sendMessageFromBytes(bytes, bytes.length); + + cancelLoad(); + } + + @Override + protected boolean isAuthTokenRejected(RtcpTokenPacket packet) { + if ((packet.getPayloadType() == RtcpPacketBuilder.RTCP_TOKEN) + && (packet.getSmt() == RtcpPacketBuilder.RTCP_SMT_TOKEN_VERIFY_FAIL)) { + + return true; + } + + return false; + } + + // Loader.Callback implementation + @Override + public void onLoadCompleted(RtpRetransmissionSource loadable, long elapsedRealtimeMs, + long loadDurationMs) { + // Do nothing + } + + @Override + public void onLoadCanceled(RtpRetransmissionSource loadable, long elapsedRealtimeMs, + long loadDurationMs, boolean released) { + eventListener.onRtpRetransmissionSourceCanceled(); + } + + @Override + public int onLoadError(RtpRetransmissionSource loadable, long elapsedRealtimeMs, + long loadDurationMs, IOException error) { + eventListener.onRtpRetransmissionSourceError(); + return Loader.DONT_RETRY; + } + } + + private final class DefaultRtpDistributionSource extends RtpDistributionSource { + + // The maximum buffer capacity, in packets. + private static final int MAX_BUFFER_CAPACITY = 2048; + + private final RtpDistributionEventListener eventListener; + + public DefaultRtpDistributionSource(int socketTimeoutMillis, + RtpDistributionEventListener eventListener) { + super(socketTimeoutMillis, eventListener); + + this.eventListener = eventListener; + } + + @Override + public int getMaxBufferCapacity() { + return MAX_BUFFER_CAPACITY; + } + + @Override + protected boolean processRtpPacket(RtpPacket packet) { + // TODO + + if (!ssrcSenderReceived) { + ssrcSender = packet.getSsrc(); + ssrcSenderReceived = true; + } + + return true; + } + + // Loader.Callback implementation + @Override + public void onLoadCompleted(RtpDistributionSource loadable, long elapsedRealtimeMs, + long loadDurationMs) { + // Do nothing + } + + @Override + public void onLoadCanceled(RtpDistributionSource loadable, long elapsedRealtimeMs, + long loadDurationMs, boolean released) { + eventListener.onRtpDistributionSourceCanceled(); + } + + @Override + public int onLoadError(RtpDistributionSource loadable, long elapsedRealtimeMs, + long loadDurationMs, IOException error) { + eventListener.onRtpDistributionSourceError(); + return Loader.DONT_RETRY; + } + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/rtp/DefaultRtpDistributionFeedbackFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/util/rtp/DefaultRtpDistributionFeedbackFactory.java new file mode 100644 index 00000000000..437f7e98056 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/rtp/DefaultRtpDistributionFeedbackFactory.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2017 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.util.rtp; + +public class DefaultRtpDistributionFeedbackFactory implements RtpDistributionFeedback.Factory { + + private final long ssrc; + private final String cname; + + public DefaultRtpDistributionFeedbackFactory(long ssrc, String cname) { + this.ssrc = ssrc; + this.cname = cname; + } + + @Override + public RtpDistributionFeedback createDistributionFeedback() { + return new DefaultRtpDistributionFeedback(ssrc, cname); + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/rtp/RtpDistributionFeedback.java b/library/core/src/main/java/com/google/android/exoplayer2/util/rtp/RtpDistributionFeedback.java new file mode 100644 index 00000000000..8877deb6faa --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/rtp/RtpDistributionFeedback.java @@ -0,0 +1,1234 @@ +/* + * Copyright (C) 2017 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.util.rtp; + +import android.net.Uri; +import android.os.ConditionVariable; + +import com.google.android.exoplayer2.upstream.Loader; +import com.google.android.exoplayer2.upstream.RtpDataSource; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.rtp.rtcp.RtcpCompoundPacket; +import com.google.android.exoplayer2.util.rtp.rtcp.RtcpFeedbackPacket; +import com.google.android.exoplayer2.util.rtp.rtcp.RtcpPacket; +import com.google.android.exoplayer2.util.rtp.rtcp.RtcpPacketBuilder; +import com.google.android.exoplayer2.util.rtp.rtcp.RtcpSdesPacket; +import com.google.android.exoplayer2.util.rtp.rtcp.RtcpSrPacket; +import com.google.android.exoplayer2.util.rtp.rtcp.RtcpTokenPacket; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.MulticastSocket; +import java.net.SocketException; +import java.net.SocketTimeoutException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * A RTP Distribution and Feedback Abstract Model interface. + */ +public interface RtpDistributionFeedback { + + int UNKNOWN_SEQ = -1; // Unknown sequence + int MAX_PACKET_SEQ = 65536; + + /** + * The feedback properties + */ + interface Properties { + int FB_SCHEME = 1; + int FB_VENDOR = 2; + int FB_RAMS_URI = 3; + int FB_CONGESTION_CONTROL_URI = 4; + int FB_PORT_MAPPING_URI = 5; + int FB_EVENTS_CALLBACK = 6; + } + + /** + * The feedback schemes + */ + interface Schemes { + int FB_REPORT = 0x01; + int FB_RAMS = 0x02; + int FB_CONGESTION_CONTROL = 0x04; + int FB_PORT_MAPPING = 0x08; + } + + /** + * The middleware providers (feedback vendors) + */ + interface Providers { + int ADTEC_DIGITAL = 1; + int ALTICAST = 2; + int ALU = 3; + int BCC = 4; + int BEE_MEDIASOFT = 5; + int BEENIUS = 6; + int CASCADE = 7; + int COMIGO = 8; + int CONKLIN_INTRACOM = 9; + int CUBIWARE = 10; + int DIGISOFT_TV = 11; + int EASY_TV = 12; + int ERICSSON = 13; + int ESPIAL = 14; + int HUAWEI = 15; + int IKON = 16; + int LEV_TV = 17; + int MICROSOFT = 18; + int MINERVA = 19; + int MIRADA = 20; + int NANGU_TV = 21; + int NETGEM = 22; + int NORDIJA = 23; + int NOKIA = 24; + int OCILION = 25; + int QUADRILLE = 26; + int SEACHANGE = 27; + int SIEMENS = 28; + int SMARTLABS = 29; + int THOMSON = 30; + int TIVO = 31; + int UTSTART = 32; + int VIANEOS = 33; + int ZAPPWARE = 34; + int ZENTERIO = 35; + int ZTE = 36; + } + + + /** + * A factory for {@link RtpDistributionFeedback} instances. + */ + interface Factory { + /** + * Creates a {@link RtpDistributionFeedback} instance. + */ + RtpDistributionFeedback createDistributionFeedback(); + } + + + /** + * A distribution and feedback event abstract + */ + interface RtpFeedbackEvent { + } + + /** + * A event listener for {@link RtpFeedbackEvent} events. + */ + interface RtpFeedbackEventListener { + /** + * Called when an event has been triggered from distribution and feedback architecture + */ + void onRtpFeedbackEvent(RtpFeedbackEvent event); + } + + + /** + * Thrown when an error is encountered when trying to create a {@link RtpFeedbackTarget} or + * {@link RtpDistributionSource}. + */ + final class UnsupportedRtpDistributionFeedbackSourceException extends Exception { + + public UnsupportedRtpDistributionFeedbackSourceException(String message) { + super(message); + } + + public UnsupportedRtpDistributionFeedbackSourceException(Throwable cause) { + super(cause); + } + + public UnsupportedRtpDistributionFeedbackSourceException(String message, Throwable cause) { + super(message, cause); + } + } + + RtpAuthTokenSource createAuthTokenSource( + RtpFeedbackTargetSource.AuthTokenEventListener eventListener) + throws UnsupportedRtpDistributionFeedbackSourceException; + + RtpBurstSource createBurstSource(RtpFeedbackTargetSource.BurstEventListener eventListener) + throws UnsupportedRtpDistributionFeedbackSourceException; + + RtpRetransmissionSource createRetransmissionSource( + RtpFeedbackTargetSource.RetransmissionEventListener eventListener) + throws UnsupportedRtpDistributionFeedbackSourceException; + + RtpDistributionSource createDistributionSource(RtpDistributionEventListener eventListener) + throws UnsupportedRtpDistributionFeedbackSourceException; + + void setFeedbackEventListener(RtpFeedbackEventListener eventListener); + + + final class RtpFeedbackProperties { + + private final Map properties; + private Map propertiesSnapshot; + + public RtpFeedbackProperties() { + properties = new HashMap<>(); + } + + /** + * Sets the specified property {@code value} for the specified {@code name}. If a property for + * this name previously existed, the old value is replaced by the specified value. + * + * @param id The identifier of the request property. + * @param value The value of the request property. + */ + public synchronized void set(Integer id, Object value) { + propertiesSnapshot = null; + properties.put(id, value); + } + + /** + * Sets the keys and values contained in the map. If a property previously existed, the old + * value is replaced by the specified value. If a property previously existed and is not in the + * map, the property is left unchanged. + * + * @param properties The request properties. + */ + public synchronized void set(Map properties) { + propertiesSnapshot = null; + properties.putAll(properties); + } + + /** + * Removes all properties previously existing and sets the keys and values of the map. + * + * @param properties The request properties. + */ + public synchronized void clearAndSet(Map properties) { + propertiesSnapshot = null; + properties.clear(); + properties.putAll(properties); + } + + /** + * Removes a request property by name. + * + * @param identifier The identifier of the request property to remove. + */ + public synchronized void remove(Integer identifier) { + propertiesSnapshot = null; + properties.remove(identifier); + } + + /** + * Clears all request properties. + */ + public synchronized void clear() { + propertiesSnapshot = null; + properties.clear(); + } + + /** + * Gets a snapshot of the request properties. + * + * @return A snapshot of the request properties. + */ + public synchronized Map getSnapshot() { + if (propertiesSnapshot == null) { + propertiesSnapshot = Collections.unmodifiableMap(new HashMap<>(properties)); + } + + return propertiesSnapshot; + } + + } + + + interface RtpFeedbackTarget { + /** + * Thrown when an error is encountered when trying to open/read/write from/to a + * {@link RtpFeedbackTarget}. + */ + final class RtpFeedbackTargetException extends IOException { + + public RtpFeedbackTargetException(IOException cause) { + super(cause); + } + + } + + void open(Uri uri) throws RtpFeedbackTargetException; + + void close(); + + Uri getUri(); + + /** + * Interface for messages to be sent into authentication source. + */ + interface AuthTokenMessages { + /** + * send a Port Mapping Request packet to Retransmission Server to initialize + * the port mapping setup procedure. (sendPortMappingRequest) + */ + void sendAuthTokenRequest() throws IOException; + } + + /** + * Interface for messages to be sent into burst source. + */ + interface BurstMessages { + /** + * send a RAMS Request packet to Retransmission Server to initialize + * the unicast-based rapid acquisition of multicast procedure. (sendRamsRequest) + */ + void sendBurstRapidAcquisitionRequest(Uri uri) throws IOException; + + /** + * send a RAMS Terminate packet to Retransmission Server to finalize the unicast-based + * rapid acquisition of multicast procedure. (sendRamsTerminate) + * + * or send a BYE packet to Retransmission Server to finalize the rtcp feedback. + */ + void sendBurstTerminationRequest() throws IOException; + } + + /** + * Interface for messages to be sent into retransmission source. + */ + interface RetransmissionMessages { + /** + * send a Generic Negative Acknowledgement packet to Retransmission Server to notify a + * packet loss was detected. (sendNack) + */ + void sendRetransmissionPacketRequest(int lastSequenceReceived, + int numLostPackets) throws IOException; + + /** + * send a BYE packet to Retransmission Server to finalize the rtcp feedback. + */ + void sendRetransmissionTerminationRequest() throws IOException; + } + } + + interface RtpFeedbackTargetSource { + + /** + * Interface for callbacks to be notified from Authentication Token Source. + */ + interface AuthTokenEventListener { + /** + * Called when a Port Mapping Response packet has been received from Retransmission + * Server. (onPortMappingResponse) + */ + void onAuthTokenResponse(); + + /** + * Called when a Port Mapping response packet has not received from Retransmission + * Server within a specified timeframe. (onPortMappingResponseBeforeTimeout) + */ + void onAuthTokenResponseBeforeTimeout(); + + /** + * Called when an error occurs decoding Port Mapping response packet received from + * Retransmission Server (onPortMappingResponseBeforeError) + */ + void onAuthTokenResponseBeforeError(); + + /** + * Called when the Port Mapping response packet received from + * Retransmission Server (onPortMappingResponseBeforeError) is unexpected + */ + void onAuthTokenResponseUnexpected(); + + /** + * Called when a RTP Authentication Token source encounters an error. + */ + void onRtpAuthTokenSourceError(); + + /** + * Called when a RTP Authentication Token source has been canceled. + */ + void onRtpAuthTokenSourceCanceled(); + } + + /** + * Interface for callbacks to be notified from Burst Source. + */ + interface BurstEventListener { + + /** + * Called when a RAMS Information packet has been accepted by Retransmission Server. + */ + void onBurstRapidAcquisitionAccepted(); + + /** + * Called when a RAMS Information packet has been rejected by Retransmission Server. + */ + void onBurstRapidAcquisitionRejected(); + + + /** + * Called when an multicast join signaling is detected that requires the RTP_Rx + * to be notified to send SFGMP Join message. + */ + void onMulticastJoinSignal(); + + /** + * Called when the rapid acquisition has been completed. + */ + void onBurstRapidAcquisitionCompleted(); + + /** + * Called when a Rams Information packet has not received from Retransmission Server + * within a specified timeframe. + */ + void oBurstRapidAcquisitionResponseBeforeTimeout(); + + /** + * Called when the Retransmission Server has detected that token is invalid or has expired. + * It is only applied when the por mapping mechanism is supported + */ + void onInvalidToken(); + + /** + * Called when a RTP packet burst has been received. + */ + void onRtpPacketBurstReceived(RtpPacket packet); + + /** + * Called when a RTP Burst Source encounters an error. + */ + void onRtpBurstSourceError(); + + /** + * Called when a RTP Burst source has been canceled. + */ + void onRtpBurstSourceCanceled(); + } + + /** + * Interface for callbacks to be notified from Retransmission Source. + */ + interface RetransmissionEventListener { + + /** + * Called when the Retransmission Server has detected that token is invalid or has expired. + * It is only applied when the por mapping mechanism is supported + */ + void onInvalidToken(); + + /** + * Called when a RTP packet loss has been received. + */ + void onRtpPacketLossReceived(RtpPacket packet); + + /** + * Called when a RTP Retransmission Source encounters an error. + */ + void onRtpRetransmissionSourceError(); + + /** + * Called when a RTP Retransmission source has been canceled. + */ + void onRtpRetransmissionSourceCanceled(); + } + } + + /** + * Interface for callbacks to be notified from Distribution Source. + */ + interface RtpDistributionEventListener { + /** + * Called when a RTP packet has been received. + */ + void onRtpPacketReceived(RtpPacket packet); + + /** + * Called when a RTP lost packet has been detected. + */ + void onRtpLostPacketDetected(int lastSequenceReceived, int numLostPackets); + + /** + * Called when a RTP Distribution source encounters an error. + */ + void onRtpDistributionSourceError(); + + /** + * Called when a RTP Distribution source has been canceled. + */ + void onRtpDistributionSourceCanceled(); + } + + abstract class RtpAuthTokenSource implements RtpFeedbackTarget, + RtpFeedbackTarget.AuthTokenMessages, + RtcpCompoundPacket.RtcpCompoundPacketEventListener, + Loader.Loadable, + Loader.Callback { + + private Uri uri; + + private byte[] inBuffer; + private byte[] outBuffer; + + protected DatagramSocket socket; + private final DatagramPacket inPacket; + protected final DatagramPacket outPacket; + + private InetAddress address; + private InetSocketAddress socketAddress; + + private final int socketTimeoutMillis; + + private final ConditionVariable loadCondition; + private volatile boolean loadCanceled = false; + + private RtpFeedbackTargetSource.AuthTokenEventListener eventListener; + + private byte[] token; + private long expirationTime; + + private boolean authReponsePending = false; + + public RtpAuthTokenSource(int socketTimeoutMillis, + RtpFeedbackTargetSource.AuthTokenEventListener eventListener) { + + this.eventListener = eventListener; + this.socketTimeoutMillis = socketTimeoutMillis; + + this.loadCondition = new ConditionVariable(false); + + this.inBuffer = new byte[RtpDataSource.MTU_SIZE]; + this.outBuffer = new byte[RtpDataSource.MTU_SIZE]; + + this.inPacket = new DatagramPacket(this.inBuffer, 0, RtpDataSource.MTU_SIZE); + this.outPacket = new DatagramPacket(this.outBuffer, 0, RtpDataSource.MTU_SIZE); + } + + public final byte[] getToken() { return token; } + + public final long getExpirationTime() { return expirationTime; } + + abstract byte[] getRandomNonce(); + + protected void sendMessageFromBytes(byte[] bytes, int length) throws IOException { + outPacket.setData(bytes, 0, length); + socket.send(outPacket); + + loadCondition.open(); + } + + public final void setAuthTokenResponsePending(boolean state) { + authReponsePending = state; + } + + + // RtpFeedbackTarget implementation + @Override + public void open(Uri uri) throws RtpFeedbackTargetException { + this.uri = Assertions.checkNotNull(uri); + + String host = uri.getHost(); + int port = uri.getPort(); + + try { + + address = InetAddress.getByName(host); + socketAddress = new InetSocketAddress(address, port); + + socket = new DatagramSocket(); + socket.connect(socketAddress); + + } catch (IOException e) { + throw new RtpFeedbackTargetException(e); + } + + try { + + socket.setSoTimeout(socketTimeoutMillis); + + } catch (SocketException e) { + throw new RtpFeedbackTargetException(e); + } + } + + @Override + public Uri getUri() { + return uri; + } + + + // RtcpCompoundPacketEventListener implementation + @Override + public void onSenderReportPacket(RtcpSrPacket packet) { + // Do nothing + } + + @Override + public void onSourceDescriptionPacket(RtcpSdesPacket packet) { + // Do nothing + } + + @Override + public void onRtpFeedbackPacket(RtcpFeedbackPacket packet) { + // Do nothing + } + + @Override + public void onTokenPacket(RtcpTokenPacket packet) { + if ((packet.getPayloadType() == RtcpPacketBuilder.RTCP_TOKEN) + && (packet.getSmt() == RtcpPacketBuilder.RTCP_SMT_PORT_MAPPING_RESP)) { + this.token = packet.getTokenElement(); + this.expirationTime = packet.getRelativeExpirationTime(); + + authReponsePending = false; + eventListener.onAuthTokenResponse(); + + } else { + eventListener.onAuthTokenResponseUnexpected(); + } + } + + private void handlePacket(byte[] buffer, int length) + throws RtcpCompoundPacket.RtcpCompoundPacketException { + RtcpCompoundPacket compoundPacket = new RtcpCompoundPacket(this); + compoundPacket.fromBytes(buffer, length); + } + + + // Loader.Loadable implementation + @Override + public void cancelLoad() { + loadCanceled = true; + } + + @Override + public boolean isLoadCanceled() { + return loadCanceled; + } + + @Override + public void load() throws IOException, InterruptedException { + while (!loadCanceled) { + + try { + + loadCondition.block(); + + if (authReponsePending) { + socket.receive(inPacket); + handlePacket(inPacket.getData(), inPacket.getLength()); + } + + } catch (RtcpCompoundPacket.RtcpCompoundPacketException e) { + + if (authReponsePending) { + authReponsePending = false; + eventListener.onAuthTokenResponseBeforeError(); + } + + } catch (SocketTimeoutException e) { + authReponsePending = false; + eventListener.onAuthTokenResponseBeforeTimeout(); + } + + if (authReponsePending) { + authReponsePending = false; + eventListener.onAuthTokenResponseBeforeError(); + } + } + } + + @Override + public void close() { + try { + + if (socket != null) { + socket.close(); + socket = null; + } + + loadCondition.close(); + + } catch (Exception e) { } + + address = null; + socketAddress = null; + } + + // Loader.Callback implementation + @Override + public void onLoadCompleted(RtpAuthTokenSource loadable, long elapsedRealtimeMs, + long loadDurationMs) { + // Do nothing + } + + @Override + public void onLoadCanceled(RtpAuthTokenSource loadable, long elapsedRealtimeMs, + long loadDurationMs, boolean released) { + eventListener.onRtpAuthTokenSourceCanceled(); + } + + @Override + public int onLoadError(RtpAuthTokenSource loadable, long elapsedRealtimeMs, + long loadDurationMs, IOException error) { + eventListener.onRtpAuthTokenSourceError(); + return Loader.DONT_RETRY; + } + } + + abstract class RtpBurstSource implements RtpFeedbackTarget, + RtpFeedbackTarget.BurstMessages, + RtcpCompoundPacket.RtcpCompoundPacketEventListener, + Loader.Loadable, + Loader.Callback { + + private Uri uri; + + private byte[] inBuffer; + private byte[] outBuffer; + + private DatagramSocket socket; + private final DatagramPacket inPacket; + private final DatagramPacket outPacket; + + private InetAddress address; + private InetSocketAddress socketAddress; + + private final int socketTimeoutMillis; + + private volatile boolean loadCanceled = false; + + private final RtpFeedbackTargetSource.BurstEventListener eventListener; + + public RtpBurstSource(int socketTimeoutMillis, + RtpFeedbackTargetSource.BurstEventListener eventListener) { + + this.eventListener = eventListener; + this.socketTimeoutMillis = socketTimeoutMillis; + + this.inBuffer = new byte[RtpDataSource.MTU_SIZE]; + this.outBuffer = new byte[RtpDataSource.MTU_SIZE]; + + this.inPacket = new DatagramPacket(this.inBuffer, 0, RtpDataSource.MTU_SIZE); + this.outPacket = new DatagramPacket(this.outBuffer, 0, RtpDataSource.MTU_SIZE); + } + + protected void sendMessageFromBytes(byte[] bytes, int length) throws IOException { + outPacket.setData(bytes, 0, length); + socket.send(outPacket); + } + + abstract public int getMaxBufferCapacity(); + + abstract boolean isRapidAcquisitionResponse(RtcpFeedbackPacket packet); + + abstract boolean isRapidAcquisitionAccepted(RtcpFeedbackPacket packet); + + abstract boolean isAuthTokenRejected(RtcpTokenPacket packet); + + abstract protected boolean processRtpPacket(RtpPacket packet); + + @Override + public void open(Uri uri) throws RtpFeedbackTargetException { + this.uri = Assertions.checkNotNull(uri); + + String host = uri.getHost(); + int port = uri.getPort(); + + try { + + address = InetAddress.getByName(host); + socketAddress = new InetSocketAddress(address, port); + + socket = new DatagramSocket(); + socket.connect(socketAddress); + + } catch (IOException e) { + throw new RtpFeedbackTargetException(e); + } + + try { + + socket.setSoTimeout(socketTimeoutMillis); + + } catch (SocketException e) { + throw new RtpFeedbackTargetException(e); + } + } + + @Override + public Uri getUri() { + return uri; + } + + + // RtcpCompoundPacketEventListener implementation + @Override + public void onSenderReportPacket(RtcpSrPacket packet) { + // Do nothing + } + + @Override + public void onSourceDescriptionPacket(RtcpSdesPacket packet) { + // Do nothing + } + + @Override + public void onRtpFeedbackPacket(RtcpFeedbackPacket packet) { + if (isRapidAcquisitionResponse(packet)) { + if (isRapidAcquisitionAccepted(packet)) { + eventListener.onBurstRapidAcquisitionAccepted(); + } else { + eventListener.onBurstRapidAcquisitionRejected(); + } + } + } + + @Override + public void onTokenPacket(RtcpTokenPacket packet) { + if (isAuthTokenRejected(packet)) { + eventListener.onInvalidToken(); + } + } + + private void handlePacket(byte[] buffer, int length) + throws RtcpCompoundPacket.RtcpCompoundPacketException { + try { + + RtpPacket rtpPacket = new RtpPacket(); + rtpPacket.fromBytes(buffer, length); + + if (processRtpPacket(rtpPacket)) { + eventListener.onRtpPacketBurstReceived(rtpPacket); + } + + } catch (RtpPacket.RtpPacketException ex) { + + RtcpCompoundPacket packet = new RtcpCompoundPacket(this); + packet.fromBytes(buffer, length); + } + } + + // Loader.Loadable implementation + @Override + public void cancelLoad() { + loadCanceled = true; + } + + @Override + public boolean isLoadCanceled() { + return loadCanceled; + } + + @Override + public void load() throws IOException, InterruptedException { + while (!loadCanceled) { + + try { + + if (!loadCanceled) { + socket.receive(inPacket); + handlePacket(inPacket.getData(), inPacket.getLength()); + } + + } catch (SocketTimeoutException se) { + throw new SocketTimeoutException(se.getMessage()); + + } catch (RtcpCompoundPacket.RtcpCompoundPacketException pe) { + // Do nothing + } + } + } + + @Override + public void close() { + try { + + if (socket != null) { + socket.close(); + socket = null; + } + + } catch (Exception e) { } + + address = null; + socketAddress = null; + } + + // Loader.Callback implementation + @Override + public void onLoadCompleted(RtpBurstSource loadable, long elapsedRealtimeMs, + long loadDurationMs) { + // Do nothing + } + + @Override + public void onLoadCanceled(RtpBurstSource loadable, long elapsedRealtimeMs, + long loadDurationMs, boolean released) { + + eventListener.onRtpBurstSourceCanceled(); + } + + @Override + public int onLoadError(RtpBurstSource loadable, long elapsedRealtimeMs, + long loadDurationMs, IOException error) { + + eventListener.onRtpBurstSourceError(); + return Loader.DONT_RETRY; + } + } + + abstract class RtpRetransmissionSource implements RtpFeedbackTarget, + RtpFeedbackTarget.RetransmissionMessages, + RtcpCompoundPacket.RtcpCompoundPacketEventListener, + Loader.Loadable, + Loader.Callback { + + private Uri uri; + + private byte[] inBuffer; + private byte[] outBuffer; + + private DatagramSocket socket; + private final DatagramPacket inPacket; + private final DatagramPacket outPacket; + + private InetAddress address; + private InetSocketAddress socketAddress; + + private volatile boolean loadCanceled = false; + + protected static final int BITMASK_LENGTH = 16; + + private final RtpFeedbackTargetSource.RetransmissionEventListener eventListener; + + public RtpRetransmissionSource(RtpFeedbackTargetSource.RetransmissionEventListener + eventListener) { + this.eventListener = eventListener; + + this.inBuffer = new byte[RtpDataSource.MTU_SIZE]; + this.outBuffer = new byte[RtpDataSource.MTU_SIZE]; + + this.inPacket = new DatagramPacket(this.inBuffer, 0, RtpDataSource.MTU_SIZE); + this.outPacket = new DatagramPacket(this.outBuffer, 0, RtpDataSource.MTU_SIZE); + } + + @Override + public void open(Uri uri) throws RtpFeedbackTargetException { + this.uri = Assertions.checkNotNull(uri); + + String host = uri.getHost(); + int port = uri.getPort(); + + try { + + address = InetAddress.getByName(host); + socketAddress = new InetSocketAddress(address, port); + + socket = new DatagramSocket(); + socket.connect(socketAddress); + + } catch (IOException e) { + throw new RtpFeedbackTargetException(e); + } + } + + @Override + public Uri getUri() { + return uri; + } + + abstract public int getMaxBufferCapacity(); + + abstract public void resetAllPacketsRecoveryPending(long timestamp); + + abstract public int getPacketsRecoveryPending(); + + abstract public int getMaxPacketsRecoveryPending(); + + abstract protected boolean processRtpPacket(RtpPacket packet); + + abstract boolean isAuthTokenRejected(RtcpTokenPacket packet); + + abstract public long getMaxDelayTimeForPending(); + + protected void sendMessageFromBytes(byte[] bytes, int length) throws IOException { + outPacket.setData(bytes, 0, length); + socket.send(outPacket); + } + + // RtcpCompoundPacketEventListener implementation + @Override + public void onSenderReportPacket(RtcpSrPacket packet) { + // Do nothing + } + + @Override + public void onSourceDescriptionPacket(RtcpSdesPacket packet) { + // Do nothing + } + + @Override + public void onRtpFeedbackPacket(RtcpFeedbackPacket packet) { + // Do nothing + } + + @Override + public void onTokenPacket(RtcpTokenPacket packet) { + if (isAuthTokenRejected(packet)) { + eventListener.onInvalidToken(); + } + } + + private void handlePacket(byte[] buffer, int length) + throws RtcpCompoundPacket.RtcpCompoundPacketException { + try { + + RtpPacket rtpPacket = new RtpPacket(); + rtpPacket.fromBytes(buffer, length); + + if (processRtpPacket(rtpPacket)) { + eventListener.onRtpPacketLossReceived(rtpPacket); + } + + } catch (RtpPacket.RtpPacketException ex) { + + RtcpCompoundPacket packet = new RtcpCompoundPacket(this); + packet.fromBytes(buffer, length); + } + } + + // Loader.Loadable implementation + @Override + public void cancelLoad() { + loadCanceled = true; + } + + @Override + public boolean isLoadCanceled() { + return loadCanceled; + } + + @Override + public void load() throws IOException, InterruptedException { + while (!loadCanceled) { + + try { + + if (!loadCanceled) { + socket.receive(inPacket); + handlePacket(inPacket.getData(), inPacket.getLength()); + } + + } catch (RtcpCompoundPacket.RtcpCompoundPacketException e) { + // Do nothing + } + } + } + + @Override + public void close() { + try { + + if (socket != null) { + socket.close(); + socket = null; + } + + } catch (Exception e) { } + + address = null; + socketAddress = null; + } + + // Loader.Callback implementation + @Override + public void onLoadCompleted(RtpRetransmissionSource loadable, long elapsedRealtimeMs, + long loadDurationMs) { + // Do nothing + } + + @Override + public void onLoadCanceled(RtpRetransmissionSource loadable, long elapsedRealtimeMs, + long loadDurationMs, boolean released) { + eventListener.onRtpRetransmissionSourceCanceled(); + } + + @Override + public int onLoadError(RtpRetransmissionSource loadable, long elapsedRealtimeMs, + long loadDurationMs, IOException error) { + eventListener.onRtpRetransmissionSourceError(); + return Loader.DONT_RETRY; + } + } + + abstract class RtpDistributionSource implements Loader.Loadable, + Loader.Callback { + private Uri uri; + + private byte[] inBuffer; + + private DatagramSocket socket; + private MulticastSocket mcastSocket; + private final DatagramPacket inPacket; + + private InetAddress address; + private InetSocketAddress socketAddress; + + private volatile boolean loadCanceled = false; + + private final int socketTimeoutMillis; + private final RtpDistributionEventListener eventListener; + + /** + * Thrown when an error is encountered when trying to open/read from a + * {@link RtpDistributionSource}. + */ + final class RtpDistributionSourceException extends IOException { + + public RtpDistributionSourceException(IOException cause) { + super(cause); + } + + } + + public RtpDistributionSource(int socketTimeoutMillis, + RtpDistributionEventListener eventListener) { + this.eventListener = eventListener; + this.socketTimeoutMillis = socketTimeoutMillis; + + this.inBuffer = new byte[RtpDataSource.MTU_SIZE]; + this.inPacket = new DatagramPacket(this.inBuffer, 0, RtpDataSource.MTU_SIZE); + } + + public void open(Uri uri) throws RtpDistributionSourceException { + this.uri = Assertions.checkNotNull(uri); + + String host = uri.getHost(); + int port = uri.getPort(); + + try { + + address = InetAddress.getByName(host); + socketAddress = new InetSocketAddress(address, port); + + if (address.isMulticastAddress()) { + mcastSocket = new MulticastSocket(socketAddress); + mcastSocket.joinGroup(address); + socket = mcastSocket; + } else { + socket = new DatagramSocket(); + socket.connect(socketAddress); + } + + } catch (IOException e) { + throw new RtpDistributionSourceException(e); + } + + try { + + socket.setSoTimeout(socketTimeoutMillis); + + } catch (SocketException e) { + throw new RtpDistributionSourceException(e); + } + } + + abstract public int getMaxBufferCapacity(); + + abstract protected boolean processRtpPacket(RtpPacket packet); + + public Uri getUri() { + return uri; + } + + private void handlePacket(byte[] buffer, int length) throws RtpPacket.RtpPacketException { + RtpPacket rtpPacket = new RtpPacket(); + rtpPacket.fromBytes(buffer, length); + + if (processRtpPacket(rtpPacket)) { + eventListener.onRtpPacketReceived(rtpPacket); + } + } + + // Loader.Loadable implementation + @Override + public final void cancelLoad() { + loadCanceled = true; + } + + @Override + public final boolean isLoadCanceled() { + return loadCanceled; + } + + @Override + public final void load() throws IOException, InterruptedException { + while (!loadCanceled) { + + try { + + if (!loadCanceled) { + socket.receive(inPacket); + handlePacket(inPacket.getData(), inPacket.getLength()); + } + + } catch (SocketTimeoutException se) { + throw new SocketTimeoutException(se.getMessage()); + + } catch (RtpPacket.RtpPacketException e) { + // Do nothing + } + } + } + + public void close() { + try { + + if (socket != null) { + socket.close(); + socket = null; + } + + } catch (Exception e) { } + + address = null; + socketAddress = null; + } + + // Loader.Callback implementation + @Override + public void onLoadCompleted(RtpDistributionSource loadable, long elapsedRealtimeMs, + long loadDurationMs) { + // Do nothing + } + + @Override + public void onLoadCanceled(RtpDistributionSource loadable, long elapsedRealtimeMs, + long loadDurationMs, boolean released) { + eventListener.onRtpDistributionSourceCanceled(); + } + + @Override + public int onLoadError(RtpDistributionSource loadable, long elapsedRealtimeMs, + long loadDurationMs, IOException error) { + eventListener.onRtpDistributionSourceError(); + return Loader.DONT_RETRY; + } + } +} \ No newline at end of file diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/rtp/RtpExtractorsFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/util/rtp/RtpExtractorsFactory.java new file mode 100644 index 00000000000..c39f8a31cd6 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/rtp/RtpExtractorsFactory.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2017 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.util.rtp; + + +import com.google.android.exoplayer2.extractor.Extractor; +import com.google.android.exoplayer2.extractor.ExtractorsFactory; +import com.google.android.exoplayer2.extractor.ts.TsExtractor; + +import static com.google.android.exoplayer2.extractor.ts.DefaultTsPayloadReaderFactory.FLAG_ALLOW_NON_IDR_KEYFRAMES; + +/** + * An {@link ExtractorsFactory} that provides an array of extractors for the following formats: + * + *
    + *
  • MPEG TS ({@link TsExtractor})
  • + *
+ */ +public final class RtpExtractorsFactory implements ExtractorsFactory { + + @Override + public synchronized Extractor[] createExtractors() { + Extractor[] extractors = new Extractor[1]; + extractors[0] = new TsExtractor(FLAG_ALLOW_NON_IDR_KEYFRAMES); + return extractors; + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/rtp/RtpPacket.java b/library/core/src/main/java/com/google/android/exoplayer2/util/rtp/RtpPacket.java new file mode 100644 index 00000000000..37b8d997464 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/rtp/RtpPacket.java @@ -0,0 +1,328 @@ +/* + * Copyright (C) 2017 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.util.rtp; + +import android.support.annotation.Nullable; +import android.util.Log; + + +/** + * This class wraps a RTP packet providing method to convert from a byte array or individual + * setter methods. + * + * A RTP packet is composed of an header and the subsequent payload. It has the following format: + * + * 0 1 2 3 + * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * |V=2|P|X| CC |M| PT | sequence number | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | timestamp | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | synchronization source (SSRC) identifier | + * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ + * | contributing source (CSRC) identifiers | + * | .... | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * + * The first twelve octets are present in every RTP packet, while the list of + * CSRC identifiers is present only when inserted by a mixer. + * + */ +public class RtpPacket { + + private static final String LOG_TAG = RtpPacket.class.getSimpleName(); + + private static final int MPEG_TS_SIG = 0x47; + private static final int RTP_MIN_SIZE = 4; + private static final int RTP_HDR_SIZE = 12; /* RFC 3550 */ + private static final int RTP_VERSION = 0x02; + + /* offset to header extension and extension length, + * as per RFC 3550 5.3.1 */ + private static final int XTLEN_OFFSET = 14; + private static final int XTSIZE = 4; + + private static final int RTP_XTHDRLEN = XTLEN_OFFSET + XTSIZE; + + private static final int CSRC_SIZE = 4; + + /* MPEG payload-type constants */ + public static final int RTP_MPA_TYPE = 0x0E; // MPEG-1 and MPEG-2 audio + public static final int RTP_MPV_TYPE = 0x20; // MPEG-1 and MPEG-2 video + public static final int RTP_MP2TS_TYPE = 0x21; // MPEG TS + public static final int RTP_DYN_TYPE = 0x63; // MPEG TS + + //Fields that compose the RTP header + private int version; + private boolean padding; + private boolean extension; + private int csrcCount; + + private boolean marker; + private int sequenceNumber; + private int payloadType; + + private long timestamp; + private long ssrc; + + private long[] csrc; + + //bitstream of the RTP header extension + private byte[] headerExtension; + + //bitstream of the RTP payload + private byte[] payload; + + private int packetSize; + + /** + * Thrown when an error is encountered when trying to parse from a {@link RtpPacket}. + */ + public static final class RtpPacketException extends Exception { + + public RtpPacketException(String message) { + super(message); + } + + public RtpPacketException(Throwable cause) { + super(cause); + } + + public RtpPacketException(String message, Throwable cause) { + super(message, cause); + } + + } + + public RtpPacket() { + // Fill default fields + version = 2; + padding = true; + marker = true; + ssrc = 0; + csrcCount = 0; + timestamp = 0; + } + + public RtpPacket(int payloadType, int sequenceNumber, long timestamp, long ssrc, + byte[] payload, @Nullable byte[] headerExtension) { + this(); + + this.payloadType = payloadType; + this.sequenceNumber = sequenceNumber; + this.timestamp = timestamp; + this.ssrc = ssrc; + this.payload = payload; + + this.extension = !(headerExtension == null); + this.headerExtension = headerExtension; + } + + public RtpPacket(byte[] buffer, int length) throws RtpPacketException { + fromBytes(buffer, length); + } + + public int getVersion() { + return version; + } + + public void setVersion(int version) { + this.version = version; + } + + public boolean isPadding() { + return padding; + } + + public void setPadding(boolean padding) { + this.padding = padding; + } + + public boolean isExtension() { + return extension; + } + + public void setExtension(boolean extension) { + this.extension = extension; + } + + public int getCsrcCount() { + return csrcCount; + } + + public void setCsrcCount(int csrcCount) { + this.csrcCount = csrcCount; + } + + public boolean isMarker() { + return marker; + } + + public void setMarker(boolean marker) { + this.marker = marker; + } + + public int getPayloadType() { + return payloadType; + } + + public void setPayloadType(int payloadType) { + this.payloadType = payloadType; + } + + public int getSequenceNumber() { + return sequenceNumber; + } + + public void setSequenceNumber(int sequenceNumber) { + this.sequenceNumber = sequenceNumber; + } + + public long getTimeStamp() {return timestamp; } + + public void setTimestamp(long timestamp) { + this.timestamp = timestamp; + } + + public long getSsrc() {return ssrc; } + + public void setSsrc(long ssrc) { + this.ssrc = ssrc; + } + + public long getTimestamp() { + return timestamp; + } + + public byte[] getHeaderExtension() { + return headerExtension; + } + + public void setHeaderExtension(byte[] headerExtension) { + this.headerExtension = headerExtension; + } + + public byte[] getPayload() { return payload; } + + public void setPayload(byte[] payload) { + this.payload = payload; + } + + public int getPacketSize() { + return packetSize; + } + + public void setPacketSize(int packetSize) { + this.packetSize = packetSize; + } + + + // Decode a RTP packet from bytes + public void fromBytes(byte[] buffer, int length) throws RtpPacketException { + int padLen = 0, headLen = 0, extLen = 0; + int frontSkip = 0, backSkip = 0; + + if( (buffer.length < RTP_MIN_SIZE) || (buffer.length < RTP_HDR_SIZE) ) { + throw new RtpPacketException("Inappropriate length=[" + buffer.length + "] of RTP packet"); + } + + // Read the packet header + version = (buffer[0] & 0xC0) >> 6; + + if (RTP_VERSION != version) { + throw new RtpPacketException("Wrong RTP version " + version + ", must be " + + RTP_VERSION); + } + + padding = ((buffer[0] & 0x20) >> 5) == 1; + extension = ((buffer[0] & 0x10) >> 4) == 1; + csrcCount = buffer[0] & 0x0F; + + headLen += RTP_HDR_SIZE + (CSRC_SIZE * csrcCount); + + marker = ((buffer[1] & 0x80) >> 7) == 1; + payloadType = buffer[1] & 0x7F; + + /* profile-based skip: adopted from vlc 0.8.6 code */ + if ((RtpPacket.RTP_MPA_TYPE == payloadType) || (RtpPacket.RTP_MPV_TYPE == payloadType)) { + headLen += 4; + } else if ((RtpPacket.RTP_MP2TS_TYPE != payloadType) && (RtpPacket.RTP_DYN_TYPE != payloadType)) { + throw new RtpPacketException("Unsupported payload type " + payloadType); + } + + frontSkip += headLen; + + if (padding) { + padLen = buffer[length - 1]; + backSkip += padLen; + } + + if (length < (frontSkip + backSkip)) { + throw new RtpPacketException("Invalid header (skip " + + (frontSkip + backSkip) + " exceeds packet length " + length); + } + + if (extension) { + if (buffer.length < RTP_XTHDRLEN) { + throw new RtpPacketException("RTP x-header requires " + (XTLEN_OFFSET + 1) + + " bytes, only " + buffer.length + " provided"); + } + + extLen = XTSIZE + + (Integer.SIZE/Byte.SIZE) * ((buffer[XTLEN_OFFSET] << 8) + buffer[XTLEN_OFFSET + 1]); + + frontSkip += extLen; + } + + //Payload Type: 33 MPEG 2 TS + if (payloadType == RtpPacket.RTP_MP2TS_TYPE) { + sequenceNumber = (buffer[2] & 0xff) * 256 + (buffer[3] & 0xff); + } else { + //Payload Type: DynamicRTP-Type 99 MPEG2TS with sequence number preceded. + sequenceNumber = (buffer[frontSkip]&0xff)*256+(buffer[frontSkip+1]&0xff); + frontSkip += 2; + } + + timestamp = ((buffer[4] & 0xff) << 24) | ((buffer[5] & 0xff) << 16) | + ((buffer[6] & 0xff) << 8) | (buffer[7] & 0xff); + + ssrc = ((buffer[8] & 0xff) << 24) | ((buffer[9] & 0xff) << 16) | + ((buffer[10] & 0xff) << 8) | (buffer[11] & 0xff); + + // CSRC list + if (csrcCount > 0) { + csrc = new long[csrcCount]; + + for (int i=0, pos=12; i < csrcCount; i++, pos+=4) { + csrc[i] = ((buffer[pos] & 0xff) << 24) | ((buffer[pos+1] & 0xff) << 16) | + ((buffer[pos+2] & 0xff) << 8) | (buffer[pos+3] & 0xff); + } + } + + // Read the extension header if present + if (extension) { + headerExtension = new byte[extLen]; + System.arraycopy(buffer, headLen, headerExtension, 0, extLen); + } + + // Read the payload + payload = new byte[length-frontSkip]; + System.arraycopy(buffer, frontSkip, payload, 0, length-frontSkip); + + packetSize = length; + } +} \ No newline at end of file diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/rtp/RtpPacketQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/util/rtp/RtpPacketQueue.java new file mode 100644 index 00000000000..3577681b541 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/rtp/RtpPacketQueue.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2017 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.util.rtp; + +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; + +/** + * A circular buffer queue for RTP packets + */ +public class RtpPacketQueue { + + private final static int MAX_SEGMENT_SIZE = 1316; + + private final ByteBuffer buffer; + private final RtpPacketQueueItem[] packets; + + private final int capacity; + + private int front; + private int back; + + private long total; + + private class RtpPacketQueueItem { + + public RtpPacket packet; + public int length; + public int offset; + + public RtpPacketQueueItem() { + this.packet = null; + this.length = 0; + this.offset = 0; + } + + public void reset() { + this.packet = null; + length = 0; + offset = 0; + } + } + + public RtpPacketQueue(int capacity) { + + this.capacity = capacity; + + total = front = back = 0; + + buffer = ByteBuffer.allocate(MAX_SEGMENT_SIZE * capacity); + + packets = new RtpPacketQueueItem[capacity]; + + for (int i = 0; i < capacity; i++) + packets[i] = new RtpPacketQueueItem(); + } + + public synchronized void reset() { + front = back = 0; + total = 0; + + buffer.rewind(); + + for (int i = 0; i < capacity; i++) + packets[i].reset(); + } + + public synchronized int push(RtpPacket packet) throws BufferUnderflowException { + if ((packet != null) && (packets[back].length == 0)) { + + int length = packet.getPayload().length; + + packets[back].packet = packet; + packets[back].length = length; + packets[back].offset = 0; + + buffer.position(back * MAX_SEGMENT_SIZE); + buffer.put(packet.getPayload()); + + total+=length; + + back = (back + 1) % capacity; + + return length; + } + + return -1; + } + + public synchronized int get(byte[] data, int offset, int length) throws BufferUnderflowException { + int size = 0; + + if ((length > 0) && (packets[front].length > 0)) { + + size = Math.min(length, packets[front].length); + + if (packets[front].length >= size) { + + buffer.position((front * MAX_SEGMENT_SIZE) + packets[front].offset); + buffer.get(data, offset, size); + + packets[front].length -= size; + + if (packets[front].length == 0) { + packets[front].reset(); + front = (front + 1) % capacity; + + } else { + packets[front].offset += size; + } + + total-=size; + } + } + + return size; + } + + public synchronized RtpPacket front() { + return packets[front].packet; + } + + public synchronized boolean isDataAvailable() { return (total > 0); } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/rtp/rtcp/RtcpCompoundPacket.java b/library/core/src/main/java/com/google/android/exoplayer2/util/rtp/rtcp/RtcpCompoundPacket.java new file mode 100644 index 00000000000..ec965e63d24 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/rtp/rtcp/RtcpCompoundPacket.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2017 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.util.rtp.rtcp; + +/** + * This class wraps a RTCP compound packet providing method to convert from a byte array and obtain + * the RTCP packets are composed + * + */ +public class RtcpCompoundPacket { + + public interface RtcpCompoundPacketEventListener { + /** + * Called when a Sender Report packet has been found while parsing. + */ + void onSenderReportPacket(RtcpSrPacket packet); + + /** + * Called when a Source Description packet has been found while parsing. + */ + void onSourceDescriptionPacket(RtcpSdesPacket packet); + + /** + * Called when a Generic RTP Feedback packet has been found while parsing. + */ + void onRtpFeedbackPacket(RtcpFeedbackPacket packet); + + /** + * Called when a TOKEN packet has been found while parsing. + */ + void onTokenPacket(RtcpTokenPacket packet); + } + + private RtcpPacket packets[]; + + private final RtcpCompoundPacketEventListener eventListener; + + /** + * Thrown when an error is encountered when trying to decode a {@link RtcpCompoundPacket}. + */ + public static final class RtcpCompoundPacketException extends Exception { + + public RtcpCompoundPacketException(String message) { + super(message); + } + + public RtcpCompoundPacketException(Throwable cause) { + super(cause); + } + + public RtcpCompoundPacketException(String message, Throwable cause) { + super(message, cause); + } + } + + public RtcpCompoundPacket(RtcpCompoundPacketEventListener eventListener) { + this.eventListener = eventListener; + } + + public RtcpPacket[] getPackets() { + return packets; + } + + // Decode a RTCP compound packet from bytes + public void fromBytes(byte[] buffer, int length) throws RtcpCompoundPacketException { + // TODO the implementation (invoking listener for each rtcp compound event + } +} + diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/rtp/rtcp/RtcpFeedbackPacket.java b/library/core/src/main/java/com/google/android/exoplayer2/util/rtp/rtcp/RtcpFeedbackPacket.java new file mode 100644 index 00000000000..d5fdb3d5cac --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/rtp/rtcp/RtcpFeedbackPacket.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2017 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.util.rtp.rtcp; + +public class RtcpFeedbackPacket extends RtcpPacket { + + protected void decodePayload(byte[] payload, int length) { + // TODO the implementation + } + + public int getFmt() { + return 0; + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/rtp/rtcp/RtcpPacket.java b/library/core/src/main/java/com/google/android/exoplayer2/util/rtp/rtcp/RtcpPacket.java new file mode 100644 index 00000000000..939f31be2e6 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/rtp/rtcp/RtcpPacket.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2017 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.util.rtp.rtcp; + +/** + * This class wraps a RTCP packet providing method to convert from and to a byte array. + * + * A RCTP packet is composed of an header and the subsequent payload. It has the following format: + * + /* + * 0 1 2 3 + * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * |V=2|P| RC | PT | length | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | SSRC of sender | + * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ + * : : + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * + * The first eight octets are present in every RTCP packet. + * + */ +abstract public class RtcpPacket { + + private static final int RTCP_VERSION = 0x02; + + private static final int RTCP_HDR_SIZE = 8; /* RFC 3550 */ + + // fields that compose the RTCP header + private int version; + private boolean padding; + private int receptionCount; + private int payloadType; + private int length; + private long ssrc; + + private byte[] payload; + + private int packetSize; + + /** + * Thrown when an error is encountered when trying to parse a {@link RtcpPacket}. + */ + public static final class RtcpPacketException extends Exception { + + public RtcpPacketException(String message) { + super(message); + } + + public RtcpPacketException(Throwable cause) { + super(cause); + } + + public RtcpPacketException(String message, Throwable cause) { + super(message, cause); + } + + } + + public RtcpPacket() { + // Fill default fields + version = 2; + padding = true; + receptionCount = 0; + ssrc = 0; + } + + public int getVersion() { + return version; + } + + public boolean isPadding() { + return padding; + } + + public int getReceptionCount() { + return receptionCount; + } + + public int getPayloadType() { + return payloadType; + } + + public int getLength() { + return length; + } + + public long getSsrc() { + return ssrc; + } + + public byte[] getPayload() { + return payload; + } + + public int getPacketSize() { + return packetSize; + } + + // Decode a RTCP packet from bytes + public void fromBytes(byte[] buffer, int length) throws RtcpPacketException { + int padLen = 0, headLen = RTCP_HDR_SIZE; + int frontSkip = 0, backSkip = 0; + + if (buffer.length < RTCP_HDR_SIZE) { + throw new RtcpPacketException("Inappropriate length=[" + buffer.length + "] of RTCP packet"); + } + + // Read the packet header + this.version = (buffer[0] & 0xC0) >> 6; + + if (RTCP_VERSION != this.version) { + throw new RtcpPacketException("Wrong RTCP version " + this.version + ", must be " + + RTCP_VERSION); + } + + this.padding = ((buffer[0] & 0x20) >> 5) == 1; + this.receptionCount = buffer[0] & 0x1F; + this.payloadType = buffer[1] & 0xFF; + this.length = (buffer[2] & 0xff) * 256 + (buffer[3] & 0xff); + + this.ssrc = ((buffer[4] & 0xff) << 24) | ((buffer[5] & 0xff) << 16) | + ((buffer[6] & 0xff) << 8) | (buffer[7] & 0xff); + + frontSkip += headLen; + + if (padding) { + padLen = buffer[length - 1]; + backSkip += padLen; + } + + if (length < (frontSkip + backSkip)) { + throw new RtcpPacketException("Invalid header (skip " + + (frontSkip + backSkip) + " exceeds packet length " + length); + } + + // we have already read 2 * 4 = 8 bytes + // out of ( length + 1 ) * 4 totals + int size = Math.min(((this.length + 1) * 4 - RTCP_HDR_SIZE), length-frontSkip); + + // Read the payload + this.payload = new byte[size]; + System.arraycopy(buffer, frontSkip, payload, 0, size); + + decodePayload(payload, size); + + this.packetSize = length; + } + + abstract void decodePayload(byte[] payload, int length); + + public byte[] toBytes() { return null; } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/rtp/rtcp/RtcpPacketBuilder.java b/library/core/src/main/java/com/google/android/exoplayer2/util/rtp/rtcp/RtcpPacketBuilder.java new file mode 100644 index 00000000000..97f33181e3a --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/rtp/rtcp/RtcpPacketBuilder.java @@ -0,0 +1,1323 @@ +/* + * Copyright (C) 2017 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.util.rtp.rtcp; + +import android.support.annotation.Nullable; +import android.util.SparseIntArray; + +import java.util.List; + +/** + * This class provides generic packet assembly and building functions for + * RTCP packets + */ +public class RtcpPacketBuilder { + + private static final byte VERSION = 2; + private static final byte PADDING = 0; + + // Payload Types + public static final int RTCP_SR = (int) 200; + public static final int RTCP_RR = (int) 201; + public static final int RTCP_SDES = (int) 202; + public static final int RTCP_BYE = (int) 203; + public static final int RTCP_APP = (int) 204; + + // Extended RTP for Real-time Transport Control Protocol Based Feedback + public static final int RTCP_RTPFB = (int) 205; + public static final int RTCP_PSFB = (int) 206; + + // Extended RTP for Real-time Transport Control Protocol Based Port Mapping between Unicast + // and Multicast RTP Sessions + public static final int RTCP_TOKEN = (int) 210; + + public static final int RTCP_SMT_PORT_MAPPING_REQ = (int) 1; + public static final int RTCP_SMT_PORT_MAPPING_RESP = (int) 2; + public static final int RTCP_SMT_TOKEN_VERIFY_REQ = (int) 3; + public static final int RTCP_SMT_TOKEN_VERIFY_FAIL = (int) 4; + + public static final int RTCP_SFMT_RAMS_REQ = (int) 1; + public static final int RTCP_SFMT_RAMS_INFO = (int) 2; + public static final int RTCP_SFMT_RAMS_TERM = (int) 3; + + private static final int RTCP_RAMS_TLV_SSRC = (int) 1; + + private static final byte RTCP_SDES_END = (byte) 0; + private static final byte RTCP_SDES_CNAME = (byte) 1; + private static final byte RTCP_SDES_NAME = (byte) 2; + private static final byte RTCP_SDES_EMAIL = (byte) 3; + private static final byte RTCP_SDES_PHONE = (byte) 4; + private static final byte RTCP_SDES_LOC = (byte) 5; + private static final byte RTCP_SDES_TOOL = (byte) 6; + private static final byte RTCP_SDES_NOTE = (byte) 7; + private static final byte RTCP_SDES_PRIV = (byte) 8; + + + public static class NackFbElement { + private int pid; + private int blp; + + public NackFbElement(int pid, int blp) { + this.pid = pid; + this.blp = blp; + } + + public int getPid() { return pid; } + + public int getBlp() { return blp; } + } + + /** + * A {@link TlvElement} represents each entry in a TLV formatted byte-array. + * + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Reserved | Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + : Value : + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + */ + public static class TlvElement { + /** + * The Type (T) field of the current TLV element. Note that for LV + * formatted byte-arrays (i.e. TLV whose Type/T size is 0) the value of + * this field is undefined. + */ + private long type; + /** + * The Length (L) field of the current TLV element. + */ + private long length; + /** + * The Value (V) field - a raw byte array representing the current TLV + * element + */ + private byte[] value; + + public TlvElement(long type, long length, @Nullable byte[] value) { + this.type = type; + this.length = length; + this.value = value; + } + + public long getType() { return type; } + + public long getLength() { return length; } + + public byte[] getValue() { return value; } + } + + + /** + * A {@link PrivateExtension} represents each entry in a Private Extension on TLV formatted + * byte-array. + * + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Reserved | Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Enterprise Number | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + : Value : + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + */ + public static class PrivateExtension extends TlvElement { + /** + * The Enterprise Number (V) field of the current TLV element. + */ + private long enterpriseNumber; + + public PrivateExtension(long type, long length, @Nullable byte[] value, + long enterpriseNumber) { + super(type, length, value); + + this.enterpriseNumber = enterpriseNumber; + } + + public long getEnterpriseNumber() { return enterpriseNumber; } + } + + /** + * Assembly a Receiver Report RTCP Packet. + * + * @param ssrc + * @return byte[] The Receiver Report Packet + */ + private static byte[] assembleRTCPReceiverReportPacket(long ssrc) { + /* + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + |V=2|P| RC | PT=RR=201 | length | header + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | SSRC of sender | + +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ + | SSRC_1 (SSRC of first source) | report + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ block + | fraction lost | cumulative number of packets lost | 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | extended highest sequence number received | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | interarrival jitter | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | last SR (LSR) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | delay since last SR (DLSR) | + +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ + | SSRC_2 (SSRC of second source) | report + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ block + : ... : 2 + +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ + | profile-specific extensions | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + */ + + final int FIXED_HEADER_SIZE = 4; // 4 bytes + + // construct the first byte containing V, P and RC + byte V_P_RC; + V_P_RC = (byte) ((VERSION << 6) | + (PADDING << 5) | + (0x00) + // take only the right most 5 bytes i.e. + // 00011111 = 0x1F + ); + + // SSRC of sender + byte[] ss = RtcpPacketUtils.longToBytes(ssrc, 4); + + // Payload Type = RR + byte[] pt = + RtcpPacketUtils.longToBytes((long) RTCP_RR, 1); + + byte[] receptionReportBlocks = + new byte [0]; + + /* TODO + receptionReportBlocks = + RtcpPacketUtils.append(receptionReportBlocks, + assembleRTCPReceptionReport());*/ + + // Each reception report is 24 bytes, so calculate the number of + // sources in the reception report block and update the reception + // block count in the header + byte receptionReports = (byte) (receptionReportBlocks.length / 24); + + // Reset the RC to reflect the number of reception report blocks + V_P_RC = (byte) (V_P_RC | (byte) (receptionReports & 0x1F)); + + byte[] length = + RtcpPacketUtils.longToBytes(((FIXED_HEADER_SIZE + ss.length + + receptionReportBlocks.length)/4)-1, 2); + + byte[] packet = new byte [1]; + packet[0] = V_P_RC; + packet = RtcpPacketUtils.append(packet, pt); + packet = RtcpPacketUtils.append(packet, length); + packet = RtcpPacketUtils.append(packet, ss); + + /* + TODO + packet = RtcpPacketUtils.append(packet, receptionReportBlocks); + */ + + return packet; + } + + /** + * Assembly an Source Description SDES RTCP Packet. + * + * @param ssrc The sincronization source + * @param cname The canonical name + * @return The SDES Packet + */ + private static byte[] assembleRTCPSourceDescriptionPacket(long ssrc, String cname) { + /* + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + |V=2|P| SC | PT=SDES=202 | length | + +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ + | SSRC/CSRC_1 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | SDES items | + | ... | + +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ + | SSRC/CSRC_2 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | SDES items | + | ... | + +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ + */ + + final int FIXED_HEADER_SIZE = 4; // 4 bytes + // construct the first byte containing V, P and SC + byte v_p_sc; + v_p_sc = (byte) ((VERSION << 6) | + (PADDING << 5) | + (0x01)); + + byte[] pt = + RtcpPacketUtils.longToBytes ((long) RTCP_SDES, 1); + + /////////////////////// Chunk 1 /////////////////////////////// + byte[] ss = + RtcpPacketUtils.longToBytes ((long) ssrc, 4); + + + //////////////////////////////////////////////// + // SDES Item #1 :CNAME + /* 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | CNAME=1 | length | user and domain name ... + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + */ + + byte item = RTCP_SDES_CNAME; + byte[] user_and_domain = new byte[cname.length()]; + user_and_domain = cname.getBytes(); + + + // Copy the CName item related fields + byte[] cnameHeader = { item, (byte) user_and_domain.length }; + + // Append the header and CName Information in the SDES Item Array + byte[] sdesItem = new byte[0] ; + sdesItem = RtcpPacketUtils.append (sdesItem, cnameHeader); + sdesItem = RtcpPacketUtils.append (sdesItem, user_and_domain); + + int padLen = RtcpPacketUtils.calculatePadLength(sdesItem.length); + + // Determine the length of the packet (section 6.4.1 "The length of + // the RTCP packet in 32 bit words minus one, including the header and + // any padding") + byte[] sdesLength = RtcpPacketUtils.longToBytes (((FIXED_HEADER_SIZE + + ss.length + sdesItem.length + padLen + 4)/4)-1, 2); + + // Assemble all the info into a packet + byte[] packet = new byte[2]; + + packet[0] = v_p_sc; + packet[1] = pt[0]; + packet = RtcpPacketUtils.append(packet, sdesLength); + packet = RtcpPacketUtils.append(packet, ss); + packet = RtcpPacketUtils.append(packet, sdesItem); + + if (padLen > 0) { + // Append necessary padding fields + byte[] padBytes = new byte[padLen]; + packet = RtcpPacketUtils.append(packet, padBytes); + } + + // Append SDES Item end field (32 bit boundary) + byte[] sdesItemEnd = new byte [4]; + packet = RtcpPacketUtils.append (packet, sdesItemEnd); + + return packet; + } + + /** + * + * Assembly a "BYE" packet (PT=BYE=203) + * + * @param ssrc The sincronization source + * @return The BYE Packet + * + */ + private static byte[] assembleRTCPByePacket(long ssrc) { + /* + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + |V=2|P| SC | PT=BYE=203 | length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | SSRC/CSRC | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + : ... : + +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ + | length | reason for leaving ... (opt) + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + */ + + final int FIXED_HEADER_SIZE = 4; // 4 bytes + // construct the first byte containing V, P and SC + byte V_P_SC; + V_P_SC = (byte) ((VERSION << 6) | + (PADDING << 5) | + (0x01) + ); + + // Generate the payload type byte + byte PT[] = RtcpPacketUtils.longToBytes((long) RTCP_BYE, 1); + + // Generate the SSRC + byte ss[] = RtcpPacketUtils.longToBytes((long) ssrc, 4); + + byte textLength [] = RtcpPacketUtils.longToBytes(0 , 1); + + // Length of the packet is number of 32 byte words - 1 + byte[] length = + RtcpPacketUtils.longToBytes(((FIXED_HEADER_SIZE + ss.length)/4)-1, 2); + + ///////////////////////// Packet Construction /////////////////////////////// + byte packet[] = new byte [1]; + + packet[0] = V_P_SC; + packet = RtcpPacketUtils.append(packet, PT); + packet = RtcpPacketUtils.append(packet, length); + packet = RtcpPacketUtils.append(packet, ss); + packet = RtcpPacketUtils.append(packet, textLength); + + return packet; + } + + /** + * + * Assembly a "APP" packet (PT=APP=204) + * + * @param ssrc The sincronization source + * @param name The application name + * @param appData The application-dependent data + * @return The APP Packet + * + */ + private static byte[] assembleRTCPAppPacket(long ssrc, String name, + byte[] appData) { + /* + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + |V=2|P| subtype | PT=APP=204 | length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | SSRC/CSRC | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | name (ASCII) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | application-dependent data ... + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + */ + + final int FIXED_HEADER_SIZE = 4; // 4 bytes + // construct the first byte containing V, P and SC + byte V_P_SC; + V_P_SC = (byte) ((VERSION << 6) | + (PADDING << 5) | + (0x00)); + + // Generate the payload type byte + byte[] PT = RtcpPacketUtils.longToBytes((long) RTCP_APP, 1); + + // Generate the SSRC + byte[] ss = RtcpPacketUtils.longToBytes((long) ssrc, 4); + + // Generate the application name + byte[] appName = name.getBytes(); + + int dataLen = appName.length + appData.length; + + byte[] length = RtcpPacketUtils.longToBytes (((FIXED_HEADER_SIZE + + ss.length + dataLen + 2)/4)-1, 2); + + ///////////////////////// Packet Construction /////////////////////////////// + byte packet[] = new byte [1]; + + packet[0] = V_P_SC; + packet = RtcpPacketUtils.append(packet, PT); + packet = RtcpPacketUtils.append(packet, length); + packet = RtcpPacketUtils.append(packet, ss); + + packet = RtcpPacketUtils.append(packet, appName); + packet = RtcpPacketUtils.append(packet, appData); + + return packet; + } + + + /** + * + * Assembly a Transport layer Feedback (Generic NACK) "RTPFB" packet (PT=RTPFB=205) + * + * @param ssrcSender The sincronization source of sender + * @param ssrcSource The sincronization source + * @param fbInformation The RTP sequence number array of the lost packets and the bitmask + * of the lost packets immediately following the RTP packet indicated + * by the pid + * @return The RTPFB Packet + * + */ + private static byte[] assembleRTCPNackPacket(long ssrcSender, long ssrcSource, + List fbInformation) { + /* + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + |V=2|P| FMT=1 | PT=205 | length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | SSRC of packet sender | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | SSRC of media source | + +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ + | PID | BLP | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + */ + + final int FIXED_HEADER_SIZE = 4; // 4 bytes + // construct the first byte containing V, P and FMT + byte V_P_FMT; + V_P_FMT = (byte) ((VERSION << 6) | + (PADDING << 5) | + (0x01)); + + // Generate the payload type byte + byte[] PT = RtcpPacketUtils.longToBytes((long) RTCP_RTPFB, 1); + + // Generate the SSRC packet sender + byte[] ssps = RtcpPacketUtils.longToBytes((long) ssrcSender, 4); + + // Generate the SSRC media source + byte[] ssms = RtcpPacketUtils.longToBytes((long) ssrcSource, 4); + + byte[] length = RtcpPacketUtils.longToBytes (((FIXED_HEADER_SIZE + + ssps.length + ssms.length + (fbInformation.size()*4) + 2)/4)-1, 2); + + + ///////////////////////// Packet Construction /////////////////////////////// + byte packet[] = new byte [1]; + + packet[0] = V_P_FMT; + packet = RtcpPacketUtils.append(packet, PT); + packet = RtcpPacketUtils.append(packet, length); + packet = RtcpPacketUtils.append(packet, ssps); + packet = RtcpPacketUtils.append(packet, ssms); + + // Generate the feedback control information (FCI) + for (int index = 0; index < fbInformation.size(); index++) { + + NackFbElement fbElement = fbInformation.get(index); + + // Generate the PID + byte[] pid = RtcpPacketUtils.longToBytes((long) fbElement.getPid(), 2); + + // Generate the BLP + byte[] blp = RtcpPacketUtils.longToBytes((long) fbElement.getPid(), 2); + + packet = RtcpPacketUtils.append(packet, pid); + packet = RtcpPacketUtils.append(packet, blp); + } + + return packet; + } + + + /** + * + * Assembly a Transport layer Feedback (RAMS Request) "RTPFB" packet (PT=RTPFB=205) + * + * @param ssrcSender The sincronization source of sender + * @param ssrcSource The sincronization source + * @param extensions The optional TLV elements + * @param privateExtensions The optional private extensions + * @return The RTPFB Packet + * + */ + private static byte[] assembleRTCPRamsRequestPacket(long ssrcSender, long ssrcSource, + List extensions, + List privateExtensions) { + /* + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + |V=2|P| FMT=6 | PT=205 | length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | SSRC of packet sender | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | SSRC of media source | + +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ + | SFMT=1 | Reserved | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + : Requested Media Sender SSRC(s) : + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + : Optional TLV-encoded Fields (and Padding, if needed) : + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + */ + + final int FIXED_FCI_SIZE = 8; // 8 bytes (4 + 4 bytes mandatory TLV element fixed to 0 length) + + final int FIXED_HEADER_SIZE = 4; // 4 bytes + // construct the first byte containing V, P and FMT + byte V_P_FMT; + V_P_FMT = (byte) ((VERSION << 6) | + (PADDING << 5) | + (0x06)); + + // Generate the payload type byte + byte[] PT = RtcpPacketUtils.longToBytes((long) RTCP_RTPFB, 1); + + // Generate the SSRC packet sender + byte[] ssps = RtcpPacketUtils.longToBytes((long) ssrcSender, 4); + + // Generate the SSRC media source + byte[] ssms = RtcpPacketUtils.longToBytes((long) ssrcSource, 4); + + // Length of the feedback control information (FCI) is composed of fixed and variable values + int var_fci_size = 0; + + for (int index = 0; index < extensions.size(); index++) { + var_fci_size += extensions.get(index).getLength(); + } + + for (int index = 0; index < privateExtensions.size(); index++) { + var_fci_size += privateExtensions.get(index).getLength(); + } + + byte[] length = RtcpPacketUtils.longToBytes (((FIXED_HEADER_SIZE + + ssps.length + ssms.length + (FIXED_FCI_SIZE + var_fci_size) + 2)/4)-1, 2); + + + ///////////////////////// Packet Construction /////////////////////////////// + byte packet[] = new byte [1]; + + packet[0] = V_P_FMT; + packet = RtcpPacketUtils.append(packet, PT); + packet = RtcpPacketUtils.append(packet, length); + packet = RtcpPacketUtils.append(packet, ssps); + packet = RtcpPacketUtils.append(packet, ssms); + + // Generate the feedback control information (FCI) + + // Generate the sub fmt type byte + byte[] SFMT = RtcpPacketUtils.longToBytes((long) RTCP_SFMT_RAMS_REQ, 1); + + // Generate the reserved byte + byte[] reserved = RtcpPacketUtils.longToBytes((long) 0, 3); + + // Generate the requested media senders byte + // (Mandatory TLV element: Length field set to 0 bytes means requesting to rapidly acquire channel) + byte[] tlv_type = RtcpPacketUtils.longToBytes((long) RTCP_RAMS_TLV_SSRC, 1); + byte[] tlv_reserved = RtcpPacketUtils.longToBytes((long) 0, 1); + byte[] tlv_length = RtcpPacketUtils.longToBytes((long) 0, 2); + + packet = RtcpPacketUtils.append(packet, SFMT); + packet = RtcpPacketUtils.append(packet, reserved); + packet = RtcpPacketUtils.append(packet, tlv_type); + packet = RtcpPacketUtils.append(packet, tlv_reserved); + packet = RtcpPacketUtils.append(packet, tlv_length); + + for (int index = 0; index < extensions.size(); index++) { + TlvElement tlvElement = extensions.get(index); + + byte[] opt_tlv_type = RtcpPacketUtils.longToBytes((long) tlvElement.getType(), 1); + byte[] opt_tlv_length = RtcpPacketUtils.longToBytes((long) tlvElement.getLength(), 2); + + packet = RtcpPacketUtils.append(packet, opt_tlv_type); + packet = RtcpPacketUtils.append(packet, tlv_reserved); + packet = RtcpPacketUtils.append(packet, opt_tlv_length); + packet = RtcpPacketUtils.append(packet, tlvElement.getValue()); + } + + for (int index = 0; index < privateExtensions.size(); index++) { + PrivateExtension privateExtension = privateExtensions.get(index); + + /** + * Represents a TLV formatted byte-array with Private Extensions. + * + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Reserved | Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Enterprise Number | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + : Value : + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + */ + + byte[] opt_tlv_type = RtcpPacketUtils.longToBytes(privateExtension.getType(), 1); + byte[] opt_tlv_length = RtcpPacketUtils.longToBytes(privateExtension.getLength(), 2); + byte[] enterpriseCode = RtcpPacketUtils.longToBytes(privateExtension.getEnterpriseNumber(), 4); + + packet = RtcpPacketUtils.append(packet, opt_tlv_type); + packet = RtcpPacketUtils.append(packet, tlv_reserved); + packet = RtcpPacketUtils.append(packet, opt_tlv_length); + packet = RtcpPacketUtils.append(packet, enterpriseCode); + packet = RtcpPacketUtils.append(packet, privateExtension.getValue()); + } + + return packet; + } + + + /** + * + * Assembly a Transport layer Feedback (RAMS Termination) "RTPFB" packet (PT=BYE=205) + * + * @param ssrcSender The sincronization source of sender + * @param ssrcSource The sincronization source + * @param extensions The optional TLV elements + * @param privateExtensions The optional private extensions + * @return The RTPFB Packet + * + */ + private static byte[] assembleRTCPRamsTerminationPacket(long ssrcSender, long ssrcSource, + List extensions, + List privateExtensions) { + /* + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + |V=2|P| FMT=6 | PT=205 | length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | SSRC of packet sender | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | SSRC of media source | + +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ + | SFMT=3 | Reserved | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + : Optional TLV-encoded Fields (and Padding, if needed) : + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + */ + + final int FIXED_HEADER_SIZE = 4; // 4 bytes + // construct the first byte containing V, P and FMT + byte V_P_FMT; + V_P_FMT = (byte) ((VERSION << 6) | + (PADDING << 5) | + (0x06)); + + // Generate the payload type byte + byte[] PT = RtcpPacketUtils.longToBytes((long) RTCP_RTPFB, 1); + + // Generate the SSRC packet sender + byte[] ssps = RtcpPacketUtils.longToBytes((long) ssrcSender, 4); + + // Generate the SSRC media source + byte[] ssms = RtcpPacketUtils.longToBytes((long) ssrcSource, 4); + + + // Length of the feedback control information (FCI) is composed of fixed and variable values + int var_fci_size = 0; + + for (int index = 0; index < extensions.size(); index++) { + var_fci_size += extensions.get(index).getLength(); + } + + for (int index = 0; index < privateExtensions.size(); index++) { + var_fci_size += privateExtensions.get(index).getLength(); + } + + byte[] length = RtcpPacketUtils.longToBytes (((FIXED_HEADER_SIZE + + ssps.length + ssms.length + (var_fci_size) + 2)/4)-1, 2); + + + ///////////////////////// Packet Construction /////////////////////////////// + byte packet[] = new byte [1]; + + packet[0] = V_P_FMT; + packet = RtcpPacketUtils.append(packet, PT); + packet = RtcpPacketUtils.append(packet, length); + packet = RtcpPacketUtils.append(packet, ssps); + packet = RtcpPacketUtils.append(packet, ssms); + + // Generate the feedback control information (FCI) + + // Generate the sub fmt type byte + byte[] SFMT = RtcpPacketUtils.longToBytes((long) RTCP_SFMT_RAMS_TERM, 1); + + // Generate the reserved byte + byte[] reserved = RtcpPacketUtils.longToBytes((long) 0, 3); + + // Generate the optional tlv elements + byte[] tlv_reserved = RtcpPacketUtils.longToBytes((long) 0, 1); + + packet = RtcpPacketUtils.append(packet, SFMT); + packet = RtcpPacketUtils.append(packet, reserved); + + for (int index = 0; index < extensions.size(); index++) { + TlvElement tlvElement = extensions.get(index); + + byte[] opt_tlv_type = RtcpPacketUtils.longToBytes((long) tlvElement.getType(), 1); + byte[] opt_tlv_length = RtcpPacketUtils.longToBytes((long) tlvElement.getLength(), 2); + + packet = RtcpPacketUtils.append(packet, opt_tlv_type); + packet = RtcpPacketUtils.append(packet, tlv_reserved); + packet = RtcpPacketUtils.append(packet, opt_tlv_length); + packet = RtcpPacketUtils.append(packet, tlvElement.getValue()); + } + + for (int index = 0; index < privateExtensions.size(); index++) { + PrivateExtension privateExtension = privateExtensions.get(index); + + /** + * Represents a TLV formatted byte-array with Private Extensions. + * + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Reserved | Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Enterprise Number | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + : Value : + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + */ + + byte[] opt_tlv_type = RtcpPacketUtils.longToBytes(privateExtension.getType(), 1); + byte[] opt_tlv_length = RtcpPacketUtils.longToBytes(privateExtension.getLength(), 2); + byte[] enterpriseCode = RtcpPacketUtils.longToBytes(privateExtension.getEnterpriseNumber(), 4); + + packet = RtcpPacketUtils.append(packet, opt_tlv_type); + packet = RtcpPacketUtils.append(packet, tlv_reserved); + packet = RtcpPacketUtils.append(packet, opt_tlv_length); + packet = RtcpPacketUtils.append(packet, enterpriseCode); + packet = RtcpPacketUtils.append(packet, privateExtension.getValue()); + } + + return packet; + } + + + /** + * + * Assembly a Lack of Synch Indication (LSI) "RTPFB" packet (PT=205, FMT=2) + * + * @param ssrcSender The sincronization source of sender + * @param ssrcSource The sincronization source + * @param bitrate The maximum bitrate of RTP stream it can accommodate + * @param extensions The optional extended parameters encoded using TLV elements + * @return The RTPFB Packet + * + */ + private static byte[] assembleRTCPLackSynchIndicationPacket(long ssrcSender, long ssrcSource, + long bitrate, + List extensions) { + /* + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + |V=2|P| FMT=2 | PT=205 | length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | SSRC of packet sender | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | SSRC of media source | + +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ + | Bitrate | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Extensions | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + */ + final int FIXED_HEADER_SIZE = 4; // 4 bytes + // construct the first byte containing V, P and FMT + byte V_P_FMT; + V_P_FMT = (byte) ((VERSION << 6) | + (PADDING << 5) | + (0x02)); + + // Generate the payload type byte + byte[] PT = RtcpPacketUtils.longToBytes((long) RTCP_RTPFB, 1); + + // Generate the SSRC packet sender + byte[] ssps = RtcpPacketUtils.longToBytes((long) ssrcSender, 4); + + // Generate the SSRC media source + byte[] ssms = RtcpPacketUtils.longToBytes((long) ssrcSource, 4); + + // Generate the SSRC media source + byte[] br = RtcpPacketUtils.longToBytes((long) bitrate, 8); + + // Length of the feedback control information (FCI) is composed of fixed and variable values + int var_fci_size = 0; + + for (int index = 0; index < extensions.size(); index++) { + var_fci_size += extensions.get(index).getLength(); + } + + byte[] length = RtcpPacketUtils.longToBytes (((FIXED_HEADER_SIZE + + ssps.length + ssms.length + (br.length + var_fci_size) + 2)/4)-1, 2); + + + ///////////////////////// Packet Construction /////////////////////////////// + byte packet[] = new byte [1]; + + packet[0] = V_P_FMT; + packet = RtcpPacketUtils.append(packet, PT); + packet = RtcpPacketUtils.append(packet, length); + packet = RtcpPacketUtils.append(packet, ssps); + packet = RtcpPacketUtils.append(packet, ssms); + + // Generate the feedback control information (FCI) + + // Generate the sub fmt type byte + byte[] SFMT = RtcpPacketUtils.longToBytes((long) RTCP_SFMT_RAMS_REQ, 1); + + // Generate the reserved byte + byte[] reserved = RtcpPacketUtils.longToBytes((long) 0, 3); + + // Generate the requested media senders byte + // (Mandatory TLV element: Length field set to 0 bytes means requesting to rapidly acquire channel) + + byte[] tlv_type = RtcpPacketUtils.longToBytes((long) RTCP_RAMS_TLV_SSRC, 1); + byte[] tlv_reserved = RtcpPacketUtils.longToBytes((long) 0, 1); + byte[] tlv_length = RtcpPacketUtils.longToBytes((long) 0, 2); + byte[] tlv_value = RtcpPacketUtils.longToBytes((long) 0, 4); + + packet = RtcpPacketUtils.append(packet, SFMT); + packet = RtcpPacketUtils.append(packet, reserved); + packet = RtcpPacketUtils.append(packet, tlv_type); + packet = RtcpPacketUtils.append(packet, tlv_reserved); + packet = RtcpPacketUtils.append(packet, tlv_length); + packet = RtcpPacketUtils.append(packet, tlv_value); + + for (int index = 0; index < extensions.size(); index++) { + TlvElement tlvElement = extensions.get(index); + + byte[] opt_tlv_type = RtcpPacketUtils.longToBytes((long) tlvElement.getType(), 1); + byte[] opt_tlv_length = RtcpPacketUtils.longToBytes((long) tlvElement.getLength(), 2); + + packet = RtcpPacketUtils.append(packet, opt_tlv_type); + packet = RtcpPacketUtils.append(packet, tlv_reserved); + packet = RtcpPacketUtils.append(packet, opt_tlv_length); + packet = RtcpPacketUtils.append(packet, tlvElement.getValue()); + } + + return packet; + } + + + /** + * + * Assembly a Synch Completed Indication (SCI) "RTPFB" packet (PT=205, FMT=4) + * + * @param ssrcSender The sincronization source of sender + * @param ssrcSource The sincronization source + * @param extensions The optional extended parameters encoded using TLV elements + * @return The RTPFB Packet + * + */ + private static byte[] assembleRTCPSynchCompletedIndicationPacket(long ssrcSender, long ssrcSource, + List extensions) { + /* + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + |V=2|P| FMT=4 | PT=205 | length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | SSRC of packet sender | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | SSRC of media source | + +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ + | Extensions | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + */ + final int FIXED_HEADER_SIZE = 4; // 4 bytes + // construct the first byte containing V, P and FMT + byte V_P_FMT; + V_P_FMT = (byte) ((VERSION << 6) | + (PADDING << 5) | + (0x04)); + + // Generate the payload type byte + byte[] PT = RtcpPacketUtils.longToBytes((long) RTCP_RTPFB, 1); + + // Generate the SSRC packet sender + byte[] ssps = RtcpPacketUtils.longToBytes((long) ssrcSender, 4); + + // Generate the SSRC media source + byte[] ssms = RtcpPacketUtils.longToBytes((long) ssrcSource, 4); + + // Length of the feedback control information (FCI) is only composed of variable values + int var_fci_size = 0; + + for (int index = 0; index < extensions.size(); index++) { + var_fci_size += extensions.get(index).getLength(); + } + + byte[] length = RtcpPacketUtils.longToBytes (((FIXED_HEADER_SIZE + + ssps.length + ssms.length + var_fci_size + 2)/4)-1, 2); + + + ///////////////////////// Packet Construction /////////////////////////////// + byte packet[] = new byte [1]; + + packet[0] = V_P_FMT; + packet = RtcpPacketUtils.append(packet, PT); + packet = RtcpPacketUtils.append(packet, length); + packet = RtcpPacketUtils.append(packet, ssps); + packet = RtcpPacketUtils.append(packet, ssms); + + // Generate the feedback control information (FCI) + + // Generate the sub fmt type byte + byte[] SFMT = RtcpPacketUtils.longToBytes((long) RTCP_SFMT_RAMS_REQ, 1); + + // Generate the reserved byte + byte[] reserved = RtcpPacketUtils.longToBytes((long) 0, 3); + + // Generate the requested media senders byte + // (Mandatory TLV element: Length field set to 0 bytes means requesting to rapidly acquire channel) + + byte[] tlv_type = RtcpPacketUtils.longToBytes((long) RTCP_RAMS_TLV_SSRC, 1); + byte[] tlv_reserved = RtcpPacketUtils.longToBytes((long) 0, 1); + byte[] tlv_length = RtcpPacketUtils.longToBytes((long) 0, 2); + byte[] tlv_value = RtcpPacketUtils.longToBytes((long) 0, 4); + + packet = RtcpPacketUtils.append(packet, SFMT); + packet = RtcpPacketUtils.append(packet, reserved); + packet = RtcpPacketUtils.append(packet, tlv_type); + packet = RtcpPacketUtils.append(packet, tlv_reserved); + packet = RtcpPacketUtils.append(packet, tlv_length); + packet = RtcpPacketUtils.append(packet, tlv_value); + + for (int index = 0; index < extensions.size(); index++) { + TlvElement tlvElement = extensions.get(index); + + byte[] opt_tlv_type = RtcpPacketUtils.longToBytes((long) tlvElement.getType(), 1); + byte[] opt_tlv_length = RtcpPacketUtils.longToBytes((long) tlvElement.getLength(), 2); + + packet = RtcpPacketUtils.append(packet, opt_tlv_type); + packet = RtcpPacketUtils.append(packet, tlv_reserved); + packet = RtcpPacketUtils.append(packet, opt_tlv_length); + packet = RtcpPacketUtils.append(packet, tlvElement.getValue()); + } + + return packet; + } + + + /** + * + * Assembly a Port Mapping Request packet (PT=TOKEN=210) + * + * @param ssrcSender The sincronization source of sender + * @param nonce The random nonce + * @return The RTPFB Packet + * + */ + private static byte[] assembleRTCPPortMappingRequestPacket(long ssrcSender, byte[] nonce) { + /* + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + |V=2|P| SMT=1 | PT=210 | length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | SSRC of packet sender | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Random | + | Nonce | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + */ + + final int FIXED_HEADER_SIZE = 4; // 4 bytes + // construct the first byte containing V, P and SFMT + byte V_P_SMT; + V_P_SMT = (byte) ((VERSION << 6) | + (PADDING << 5) | + (0x01)); + + // Generate the payload type byte + byte[] PT = RtcpPacketUtils.longToBytes((long) RTCP_TOKEN, 1); + + // Generate the SSRC packet sender + byte[] ssps = RtcpPacketUtils.longToBytes((long) ssrcSender, 4); + + int padLen = RtcpPacketUtils.calculate64PadLength(nonce.length); + + byte[] length = RtcpPacketUtils.longToBytes (((FIXED_HEADER_SIZE + + ssps.length + nonce.length + padLen + 2)/4)-1, 2); + + ///////////////////////// Packet Construction /////////////////////////////// + byte packet[] = new byte [1]; + + packet[0] = V_P_SMT; + packet = RtcpPacketUtils.append(packet, PT); + packet = RtcpPacketUtils.append(packet, length); + packet = RtcpPacketUtils.append(packet, ssps); + + packet = RtcpPacketUtils.append(packet, nonce); + + if (padLen > 0) { + // Append necessary padding fields + byte[] padBytes = new byte[padLen]; + packet = RtcpPacketUtils.append(packet, padBytes); + } + + return packet; + } + + + /** + * + * Assembly a Token Verification Request packet (PT=TOKEN=210) + * + * @param ssrcSender The sincronization source of sender + * @param nonce The random nonce + * @param token The authentication token (received into Port Mapping Response packet) + * @param expirationTime The expiration time for authentication token + * @return The RTPFB Packet + * + */ + private static byte[] assembleRTCPTokenVerificationRequestPacket(long ssrcSender, byte[] nonce, + byte[] token, + long expirationTime) { + /* + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + |V=2|P| SMT=3 | PT=210 | length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | SSRC of packet sender | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Random | + | Nonce | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + : Token Element : + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Associated Absolute | + | Expiration Time | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + */ + + final int FIXED_HEADER_SIZE = 4; // 4 bytes + // construct the first byte containing V, P and SFMT + byte V_P_SMT; + V_P_SMT = (byte) ((VERSION << 6) | + (PADDING << 5) | + (0x03)); + + // Generate the payload type byte + byte[] PT = RtcpPacketUtils.longToBytes((long) RTCP_TOKEN, 1); + + // Generate the SSRC packet sender + byte[] ssps = RtcpPacketUtils.longToBytes((long) ssrcSender, 4); + + int noncePadLen = RtcpPacketUtils.calculate64PadLength(nonce.length); + + byte[] expiration = RtcpPacketUtils.longToBytes((long) expirationTime, 8); + + int tokenPadLen = RtcpPacketUtils.calculatePadLength(token.length); + + byte[] length = RtcpPacketUtils.longToBytes(((FIXED_HEADER_SIZE + + ssps.length + nonce.length + noncePadLen + token.length + tokenPadLen + + expiration.length + 2) / 4) - 1, 2); + + ///////////////////////// Packet Construction /////////////////////////////// + byte packet[] = new byte[1]; + + packet[0] = V_P_SMT; + packet = RtcpPacketUtils.append(packet, PT); + packet = RtcpPacketUtils.append(packet, length); + packet = RtcpPacketUtils.append(packet, ssps); + + packet = RtcpPacketUtils.append(packet, nonce); + + if (noncePadLen > 0) { + // Append necessary padding fields + byte[] padBytes = new byte[noncePadLen]; + packet = RtcpPacketUtils.append(packet, padBytes); + } + + packet = RtcpPacketUtils.append(packet, token); + + if (tokenPadLen > 0) { + // Append necessary padding fields + byte[] padBytes = new byte[tokenPadLen]; + packet = RtcpPacketUtils.append(packet, padBytes); + } + + packet = RtcpPacketUtils.append(packet, expiration); + + return packet; + } + + + //************************************************************************ + // IETF RFC 3350 - RTP: A Transport Protocol for Real-Time Applications + + /** + * + * Constructs a "BYE" packet (PT=BYE=203) + * + * @param ssrc The sincronization source + * @param cname The canonical name + * @param cname The canonical name + * @return The BYE Packet + * + */ + public static byte[] buildByePacket(long ssrc, String cname) { + byte packet[] = new byte [0]; + + packet = RtcpPacketUtils.append(packet, assembleRTCPReceiverReportPacket(ssrc)); + packet = RtcpPacketUtils.append(packet, assembleRTCPSourceDescriptionPacket(ssrc, cname)); + packet = RtcpPacketUtils.append(packet, assembleRTCPByePacket(ssrc)); + + return packet; + } + + /** + * + * Constructs a "APP" packet (PT=APP=204) + * + * @param ssrc The sincronization source + * @param cname The canonical name + * @return The APP Packet + * + */ + public static byte[] buildAppPacket(long ssrc, String cname, + String appName, byte[] appData) { + byte packet[] = new byte [0]; + + packet = RtcpPacketUtils.append(packet, assembleRTCPReceiverReportPacket(ssrc)); + packet = RtcpPacketUtils.append(packet, assembleRTCPSourceDescriptionPacket(ssrc, cname)); + packet = RtcpPacketUtils.append(packet, assembleRTCPAppPacket(ssrc, appName, appData)); + + return packet; + } + + + //************************************************************************ + // IETF RFC 4584 - Extended RTP Profile for Real-time Transport Control Protocol (RTCP) - + // Based Feedback (RTP/AVPF) + + /** + * + * Constructs a Transport layer Feedback (Generic NACK) "RTPFB" packet (PT=RTPFB=205) + * + * @param ssrc The sincronization source + * @param cname The canonical name + * @param ssrcSender The sincronization source of sender + * @param fbInformation The RTP sequence number array of the lost packets and the bitmask + * of the lost packets immediately following the RTP packet indicated + * by the pid + * @return The RTPFB Packet + * + */ + + public static byte[] buildNackPacket(long ssrc, String cname, long ssrcSender, + List fbInformation) { + byte packet[] = new byte [0]; + + packet = RtcpPacketUtils.append(packet, assembleRTCPReceiverReportPacket(ssrc)); + packet = RtcpPacketUtils.append(packet, assembleRTCPSourceDescriptionPacket(ssrc, cname)); + packet = RtcpPacketUtils.append(packet, assembleRTCPNackPacket(ssrc, ssrcSender, + fbInformation)); + + return packet; + } + + + //************************************************************************** + // IETF RFC 6285 - Unicast-Based Rapid Acquisition of Multicast RTP Sessions + + /** + * + * Constructs a Transport layer Feedback (RAMS request) "RTPFB" packet (PT=RTPFB=205) + * + * @param ssrc The sincronization source + * @param cname The canonical name + * @param ssrcSender The sincronization source of sender + * @param extensions The optional TLV elements + * @param privateExtensions The optional private extensions + * @return The RTPFB Packet + * + */ + public static byte[] buildRamsRequestPacket(long ssrc, String cname, long ssrcSender, + List extensions, + List privateExtensions) { + byte packet[] = new byte [0]; + + packet = RtcpPacketUtils.append(packet, assembleRTCPReceiverReportPacket(ssrc)); + packet = RtcpPacketUtils.append(packet, assembleRTCPSourceDescriptionPacket(ssrc, cname)); + packet = RtcpPacketUtils.append(packet, assembleRTCPRamsRequestPacket(ssrc, ssrcSender, + extensions, privateExtensions)); + + return packet; + } + + /** + * + * Constructs a Transport layer Feedback (RAMS Termination) "RTPFB" packet (PT=RTPFB=205) + * + * @param ssrc The sincronization source + * @param cname The canonical name + * @param ssrcSender The sincronization source of sender + * @param extensions The optional TLV elements + * @param privateExtensions The optional private extensions + * @return The RTPFB Packet + * + */ + public static byte[] buildRamsTerminationPacket(long ssrc, String cname, long ssrcSender, + List extensions, + List privateExtensions) { + byte packet[] = new byte [0]; + + packet = RtcpPacketUtils.append(packet, assembleRTCPReceiverReportPacket(ssrc)); + packet = RtcpPacketUtils.append(packet, assembleRTCPSourceDescriptionPacket(ssrc, cname)); + packet = RtcpPacketUtils.append(packet, assembleRTCPRamsTerminationPacket(ssrc, ssrcSender, + extensions, privateExtensions)); + + return packet; + } + + + //************************************************************************ + // IETF RFC 6284 - Port Mapping between Unicast and Multicast RTP Sessions + + /** + * + * Constructs a Port Mapping Request packet (PT=TOKEN) + * + * @param ssrc The sincronization source + * @param cname The canonical name + * @param nonce The random nonce + * @return The Packet + * + */ + public static byte[] buildPortMappingRequestPacket(long ssrc, String cname, byte[] nonce) { + byte packet[] = new byte [0]; + + packet = RtcpPacketUtils.append(packet, assembleRTCPReceiverReportPacket(ssrc)); + packet = RtcpPacketUtils.append(packet, assembleRTCPSourceDescriptionPacket(ssrc, cname)); + packet = RtcpPacketUtils.append(packet, assembleRTCPPortMappingRequestPacket(ssrc, nonce)); + + return packet; + } + + /** + * + * Constructs a Token Verification Request packet (PT=TOKEN) + * + * @param ssrc The sincronization source + * @param cname The canonical name + * @param nonce The random nonce + * @param token The token element + * @param expirationTime The absoluete expiration time + * @return The Packet + * + */ + public static byte[] buildTokenVerificationRequestPacket(long ssrc, String cname, byte[] nonce, + byte[] token, long expirationTime) { + byte packet[] = new byte [0]; + + packet = RtcpPacketUtils.append(packet, assembleRTCPReceiverReportPacket(ssrc)); + packet = RtcpPacketUtils.append(packet, assembleRTCPSourceDescriptionPacket(ssrc, cname)); + packet = RtcpPacketUtils.append(packet, assembleRTCPTokenVerificationRequestPacket( + ssrc, nonce, token, expirationTime)); + + return packet; + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/rtp/rtcp/RtcpPacketUtils.java b/library/core/src/main/java/com/google/android/exoplayer2/util/rtp/rtcp/RtcpPacketUtils.java new file mode 100644 index 00000000000..420588bf03b --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/rtp/rtcp/RtcpPacketUtils.java @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2017 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.util.rtp.rtcp; + +/** + * This class provides generic utility functions for + * RTCP packets + */ +public class RtcpPacketUtils { + /** + * Append two byte arrays. + * Appends packet B at the end of Packet A (Assuming Bytes as elements). + * Returns packet ( AB ). + * + * @param packetA The first packet. + * @param packetB The second packet. + * @return The desired packet which is A concatenated with B. + */ + public static synchronized byte[] append(byte[] packetA, + byte[] packetB) { + // Create a new array whose size is equal to sum of packets + // being added + byte[] packetAB = new byte [ packetA.length + packetB.length ]; + + // First paste in packetA + for ( int i=0; i < packetA.length; i++ ) { + packetAB [i] = packetA [i]; + } + + // Now start pasting packetB + for ( int i=0; i < packetB.length; i++ ) { + packetAB [i+packetA.length] = packetB [i]; + } + + return packetAB; + } + + /** + * Convert signed int to long by taking 2's complement if necessary. + * + * @param intToConvert The signed integer which will be converted to Long. + * + * @return The unsigned long representation of the signed int. + */ + public static synchronized long convertSignedIntToLong(int intToConvert) + { + int in = intToConvert; + // IP: Removed + // Session.outprintln (String.valueOf(in)); + in = ( in << 1 ) >> 1; + long lin = (long) in; + lin = lin + 0x7FFFFFFF; + + return lin; + } + + /** + * Convert 64 bit long to n bytes. + * + * @param ldata The long from which the n byte array will be constructed. + * @param n The desired number of bytes to convert the long to. + * + * @return The desired byte array which is populated with the long value. + */ + public static synchronized byte[] longToBytes(long ldata, int n) + { + byte[] buff = new byte [ n ]; + for ( int i=n-1; i>=0; i--) { + // Keep assigning the right most 8 bits to the + // byte arrays while shift 8 bits during each iteration + buff [ i ] = (byte) ldata; + ldata = ldata >> 8; + } + + return buff; + } + + /** + * Calculate number of octets required to fit the + * given number of octets into 32 bit boundary. + * + * @param lengthOfUnpaddedPacket The length of an unpadded packet + * + * @return The required number of octets which must be appended to this + * packet to make it fit into a 32 bit boundary. + */ + public static synchronized int calculatePadLength(int lengthOfUnpaddedPacket) { + // Determine the number of 8 bit words required to fit the packet in + // 32 bit boundary + int remain = + (int) Math.IEEEremainder ( (double) lengthOfUnpaddedPacket , + (double) 4 ); + int padLen = 0; + // e.g. remainder -1, then we need to pad 1 extra + // byte to the end to make it to the 32 bit boundary + if ( remain < 0 ) + padLen = Math.abs ( remain ); + // e.g. remainder is +1 then we need to pad 3 extra bytes + else if ( remain > 0 ) + padLen = 4-remain; + + return ( padLen ); + } + + /** + * Calculate number of octets required to fit the + * given number of octets into 64 bit boundary. + * + * @param lengthOfUnpaddedPacket The length of an unpadded packet + * + * @return The required number of octets which must be appended to this + * packet to make it fit into a 64 bit boundary. + */ + public static synchronized int calculate64PadLength(int lengthOfUnpaddedPacket) { + // Determine the number of 8 bit words required to fit the packet in + // 64 bit boundary + int remain = + (int) Math.IEEEremainder ( (double) lengthOfUnpaddedPacket , + (double) 16 ); + int padLen = 0; + // e.g. remainder -1, then we need to pad 1 extra + // byte to the end to make it to the 64 bit boundary + if ( remain < 0 ) + padLen = Math.abs ( remain ); + // e.g. remainder is +1 then we need to pad 3 extra bytes + else if ( remain > 0 ) + padLen = 16-remain; + + return ( padLen ); + } + + /** Byte swap */ + + public static synchronized byte[] swapBytes(byte[] bdata) + { + byte[] buff = new byte [ bdata.length ]; + for ( int i=bdata.length-1; i>=0; i--) { + buff [ i ] = bdata[(bdata.length -1) - i ]; + } + + return buff; + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/rtp/rtcp/RtcpSdesPacket.java b/library/core/src/main/java/com/google/android/exoplayer2/util/rtp/rtcp/RtcpSdesPacket.java new file mode 100644 index 00000000000..99bad6a2ddb --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/rtp/rtcp/RtcpSdesPacket.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2017 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.util.rtp.rtcp; + +public class RtcpSdesPacket extends RtcpPacket { + + void decodePayload(byte[] payload, int length) { + // TODO the implementation + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/rtp/rtcp/RtcpSessionUtils.java b/library/core/src/main/java/com/google/android/exoplayer2/util/rtp/rtcp/RtcpSessionUtils.java new file mode 100644 index 00000000000..050d31a6e87 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/rtp/rtcp/RtcpSessionUtils.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2017 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.util.rtp.rtcp; + +import com.google.android.exoplayer2.util.net.Connectivity; +import com.google.android.exoplayer2.util.net.NetworkUtils; + +/** + * This class provides generic utility functions for + * RTCP sessions + */ +public class RtcpSessionUtils { + + public static long SSRC() { + return (long)((65535L * Math.random()) + 1L); + } + + public static String CNAME() { + String iface = "eth0"; + + try { + + iface = Connectivity.isConnectedEthernet() ? "eth0" : "wlan0"; + } catch (Exception e) { + + } + + StringBuilder cname = new StringBuilder(); + String macAddress = NetworkUtils.getMACAddress(iface); + String[] tokensMacAddr = macAddress.split(":"); + + //TODO append urn:uuid: + + cname.append(tokensMacAddr[1]); + cname.append(tokensMacAddr[2]); + cname.append(tokensMacAddr[3]); + cname.append('@'); + cname.append(tokensMacAddr[4]); + + return cname.toString(); + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/rtp/rtcp/RtcpSrPacket.java b/library/core/src/main/java/com/google/android/exoplayer2/util/rtp/rtcp/RtcpSrPacket.java new file mode 100644 index 00000000000..aa20695f540 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/rtp/rtcp/RtcpSrPacket.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2017 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.util.rtp.rtcp; + +public class RtcpSrPacket extends RtcpPacket { + + void decodePayload(byte[] payload, int length) { + // TODO the implementation + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/rtp/rtcp/RtcpTokenPacket.java b/library/core/src/main/java/com/google/android/exoplayer2/util/rtp/rtcp/RtcpTokenPacket.java new file mode 100644 index 00000000000..efbcf84d571 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/rtp/rtcp/RtcpTokenPacket.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2017 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.util.rtp.rtcp; + +public class RtcpTokenPacket extends RtcpPacket { + + protected void decodePayload(byte[] payload, int length) { + // TODO the implementation + } + + public byte[] getTokenElement() { return null; } + + public int getSmt() { return 0; } + + public long getRelativeExpirationTime() { + return 0L; + } +}