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; + } +}