| Index: talk/examples/android/src/org/appspot/apprtc/PeerConnectionClient.java
|
| diff --git a/talk/examples/android/src/org/appspot/apprtc/PeerConnectionClient.java b/talk/examples/android/src/org/appspot/apprtc/PeerConnectionClient.java
|
| deleted file mode 100644
|
| index e3cdc99e239d31355300dd0feed46e1c68f750cc..0000000000000000000000000000000000000000
|
| --- a/talk/examples/android/src/org/appspot/apprtc/PeerConnectionClient.java
|
| +++ /dev/null
|
| @@ -1,1033 +0,0 @@
|
| -/*
|
| - * libjingle
|
| - * Copyright 2014 Google Inc.
|
| - *
|
| - * Redistribution and use in source and binary forms, with or without
|
| - * modification, are permitted provided that the following conditions are met:
|
| - *
|
| - * 1. Redistributions of source code must retain the above copyright notice,
|
| - * this list of conditions and the following disclaimer.
|
| - * 2. Redistributions in binary form must reproduce the above copyright notice,
|
| - * this list of conditions and the following disclaimer in the documentation
|
| - * and/or other materials provided with the distribution.
|
| - * 3. The name of the author may not be used to endorse or promote products
|
| - * derived from this software without specific prior written permission.
|
| - *
|
| - * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
|
| - * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
| - * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
|
| - * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
| - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
| - * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
|
| - * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
| - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
|
| - * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
|
| - * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
| - */
|
| -
|
| -package org.appspot.apprtc;
|
| -
|
| -import android.content.Context;
|
| -import android.opengl.EGLContext;
|
| -import android.util.Log;
|
| -
|
| -import org.appspot.apprtc.AppRTCClient.SignalingParameters;
|
| -import org.appspot.apprtc.util.LooperExecutor;
|
| -import org.webrtc.DataChannel;
|
| -import org.webrtc.IceCandidate;
|
| -import org.webrtc.Logging;
|
| -import org.webrtc.MediaCodecVideoEncoder;
|
| -import org.webrtc.MediaConstraints;
|
| -import org.webrtc.MediaConstraints.KeyValuePair;
|
| -import org.webrtc.MediaStream;
|
| -import org.webrtc.PeerConnection;
|
| -import org.webrtc.PeerConnection.IceConnectionState;
|
| -import org.webrtc.PeerConnectionFactory;
|
| -import org.webrtc.SdpObserver;
|
| -import org.webrtc.SessionDescription;
|
| -import org.webrtc.StatsObserver;
|
| -import org.webrtc.StatsReport;
|
| -import org.webrtc.VideoCapturerAndroid;
|
| -import org.webrtc.VideoRenderer;
|
| -import org.webrtc.VideoSource;
|
| -import org.webrtc.VideoTrack;
|
| -
|
| -import java.util.EnumSet;
|
| -import java.util.LinkedList;
|
| -import java.util.Timer;
|
| -import java.util.TimerTask;
|
| -import java.util.regex.Matcher;
|
| -import java.util.regex.Pattern;
|
| -
|
| -/**
|
| - * Peer connection client implementation.
|
| - *
|
| - * <p>All public methods are routed to local looper thread.
|
| - * All PeerConnectionEvents callbacks are invoked from the same looper thread.
|
| - * This class is a singleton.
|
| - */
|
| -public class PeerConnectionClient {
|
| - public static final String VIDEO_TRACK_ID = "ARDAMSv0";
|
| - public static final String AUDIO_TRACK_ID = "ARDAMSa0";
|
| - private static final String TAG = "PCRTCClient";
|
| - private static final String FIELD_TRIAL_VP9 = "WebRTC-SupportVP9/Enabled/";
|
| - private static final String VIDEO_CODEC_VP8 = "VP8";
|
| - private static final String VIDEO_CODEC_VP9 = "VP9";
|
| - private static final String VIDEO_CODEC_H264 = "H264";
|
| - private static final String AUDIO_CODEC_OPUS = "opus";
|
| - private static final String AUDIO_CODEC_ISAC = "ISAC";
|
| - private static final String VIDEO_CODEC_PARAM_START_BITRATE =
|
| - "x-google-start-bitrate";
|
| - private static final String AUDIO_CODEC_PARAM_BITRATE = "maxaveragebitrate";
|
| - private static final String AUDIO_ECHO_CANCELLATION_CONSTRAINT = "googEchoCancellation";
|
| - private static final String AUDIO_AUTO_GAIN_CONTROL_CONSTRAINT= "googAutoGainControl";
|
| - private static final String AUDIO_HIGH_PASS_FILTER_CONSTRAINT = "googHighpassFilter";
|
| - private static final String AUDIO_NOISE_SUPPRESSION_CONSTRAINT = "googNoiseSuppression";
|
| - private static final String MAX_VIDEO_WIDTH_CONSTRAINT = "maxWidth";
|
| - private static final String MIN_VIDEO_WIDTH_CONSTRAINT = "minWidth";
|
| - private static final String MAX_VIDEO_HEIGHT_CONSTRAINT = "maxHeight";
|
| - private static final String MIN_VIDEO_HEIGHT_CONSTRAINT = "minHeight";
|
| - private static final String MAX_VIDEO_FPS_CONSTRAINT = "maxFrameRate";
|
| - private static final String MIN_VIDEO_FPS_CONSTRAINT = "minFrameRate";
|
| - private static final String DTLS_SRTP_KEY_AGREEMENT_CONSTRAINT = "DtlsSrtpKeyAgreement";
|
| - private static final int HD_VIDEO_WIDTH = 1280;
|
| - private static final int HD_VIDEO_HEIGHT = 720;
|
| - private static final int MAX_VIDEO_WIDTH = 1280;
|
| - private static final int MAX_VIDEO_HEIGHT = 1280;
|
| - private static final int MAX_VIDEO_FPS = 30;
|
| -
|
| - private static final PeerConnectionClient instance = new PeerConnectionClient();
|
| - private final PCObserver pcObserver = new PCObserver();
|
| - private final SDPObserver sdpObserver = new SDPObserver();
|
| - private final LooperExecutor executor;
|
| -
|
| - private PeerConnectionFactory factory;
|
| - private PeerConnection peerConnection;
|
| - PeerConnectionFactory.Options options = null;
|
| - private VideoSource videoSource;
|
| - private boolean videoCallEnabled;
|
| - private boolean preferIsac;
|
| - private boolean preferH264;
|
| - private boolean videoSourceStopped;
|
| - private boolean isError;
|
| - private Timer statsTimer;
|
| - private VideoRenderer.Callbacks localRender;
|
| - private VideoRenderer.Callbacks remoteRender;
|
| - private SignalingParameters signalingParameters;
|
| - private MediaConstraints pcConstraints;
|
| - private MediaConstraints videoConstraints;
|
| - private MediaConstraints audioConstraints;
|
| - private MediaConstraints sdpMediaConstraints;
|
| - private PeerConnectionParameters peerConnectionParameters;
|
| - // Queued remote ICE candidates are consumed only after both local and
|
| - // remote descriptions are set. Similarly local ICE candidates are sent to
|
| - // remote peer after both local and remote description are set.
|
| - private LinkedList<IceCandidate> queuedRemoteCandidates;
|
| - private PeerConnectionEvents events;
|
| - private boolean isInitiator;
|
| - private SessionDescription localSdp; // either offer or answer SDP
|
| - private MediaStream mediaStream;
|
| - private int numberOfCameras;
|
| - private VideoCapturerAndroid videoCapturer;
|
| - // enableVideo is set to true if video should be rendered and sent.
|
| - private boolean renderVideo;
|
| - private VideoTrack localVideoTrack;
|
| - private VideoTrack remoteVideoTrack;
|
| -
|
| - /**
|
| - * Peer connection parameters.
|
| - */
|
| - public static class PeerConnectionParameters {
|
| - public final boolean videoCallEnabled;
|
| - public final boolean loopback;
|
| - public final int videoWidth;
|
| - public final int videoHeight;
|
| - public final int videoFps;
|
| - public final int videoStartBitrate;
|
| - public final String videoCodec;
|
| - public final boolean videoCodecHwAcceleration;
|
| - public final int audioStartBitrate;
|
| - public final String audioCodec;
|
| - public final boolean noAudioProcessing;
|
| - public final boolean cpuOveruseDetection;
|
| -
|
| - public PeerConnectionParameters(
|
| - boolean videoCallEnabled, boolean loopback,
|
| - int videoWidth, int videoHeight, int videoFps, int videoStartBitrate,
|
| - String videoCodec, boolean videoCodecHwAcceleration,
|
| - int audioStartBitrate, String audioCodec,
|
| - boolean noAudioProcessing, boolean cpuOveruseDetection) {
|
| - this.videoCallEnabled = videoCallEnabled;
|
| - this.loopback = loopback;
|
| - this.videoWidth = videoWidth;
|
| - this.videoHeight = videoHeight;
|
| - this.videoFps = videoFps;
|
| - this.videoStartBitrate = videoStartBitrate;
|
| - this.videoCodec = videoCodec;
|
| - this.videoCodecHwAcceleration = videoCodecHwAcceleration;
|
| - this.audioStartBitrate = audioStartBitrate;
|
| - this.audioCodec = audioCodec;
|
| - this.noAudioProcessing = noAudioProcessing;
|
| - this.cpuOveruseDetection = cpuOveruseDetection;
|
| - }
|
| - }
|
| -
|
| - /**
|
| - * Peer connection events.
|
| - */
|
| - public static interface PeerConnectionEvents {
|
| - /**
|
| - * Callback fired once local SDP is created and set.
|
| - */
|
| - public void onLocalDescription(final SessionDescription sdp);
|
| -
|
| - /**
|
| - * Callback fired once local Ice candidate is generated.
|
| - */
|
| - public void onIceCandidate(final IceCandidate candidate);
|
| -
|
| - /**
|
| - * Callback fired once connection is established (IceConnectionState is
|
| - * CONNECTED).
|
| - */
|
| - public void onIceConnected();
|
| -
|
| - /**
|
| - * Callback fired once connection is closed (IceConnectionState is
|
| - * DISCONNECTED).
|
| - */
|
| - public void onIceDisconnected();
|
| -
|
| - /**
|
| - * Callback fired once peer connection is closed.
|
| - */
|
| - public void onPeerConnectionClosed();
|
| -
|
| - /**
|
| - * Callback fired once peer connection statistics is ready.
|
| - */
|
| - public void onPeerConnectionStatsReady(final StatsReport[] reports);
|
| -
|
| - /**
|
| - * Callback fired once peer connection error happened.
|
| - */
|
| - public void onPeerConnectionError(final String description);
|
| - }
|
| -
|
| - private PeerConnectionClient() {
|
| - executor = new LooperExecutor();
|
| - // Looper thread is started once in private ctor and is used for all
|
| - // peer connection API calls to ensure new peer connection factory is
|
| - // created on the same thread as previously destroyed factory.
|
| - executor.requestStart();
|
| - }
|
| -
|
| - public static PeerConnectionClient getInstance() {
|
| - return instance;
|
| - }
|
| -
|
| - public void setPeerConnectionFactoryOptions(PeerConnectionFactory.Options options) {
|
| - this.options = options;
|
| - }
|
| -
|
| - public void createPeerConnectionFactory(
|
| - final Context context,
|
| - final EGLContext renderEGLContext,
|
| - final PeerConnectionParameters peerConnectionParameters,
|
| - final PeerConnectionEvents events) {
|
| - this.peerConnectionParameters = peerConnectionParameters;
|
| - this.events = events;
|
| - videoCallEnabled = peerConnectionParameters.videoCallEnabled;
|
| - // Reset variables to initial states.
|
| - factory = null;
|
| - peerConnection = null;
|
| - preferIsac = false;
|
| - preferH264 = false;
|
| - videoSourceStopped = false;
|
| - isError = false;
|
| - queuedRemoteCandidates = null;
|
| - localSdp = null; // either offer or answer SDP
|
| - mediaStream = null;
|
| - videoCapturer = null;
|
| - renderVideo = true;
|
| - localVideoTrack = null;
|
| - remoteVideoTrack = null;
|
| - statsTimer = new Timer();
|
| -
|
| - executor.execute(new Runnable() {
|
| - @Override
|
| - public void run() {
|
| - createPeerConnectionFactoryInternal(context, renderEGLContext);
|
| - }
|
| - });
|
| - }
|
| -
|
| - public void createPeerConnection(
|
| - final VideoRenderer.Callbacks localRender,
|
| - final VideoRenderer.Callbacks remoteRender,
|
| - final SignalingParameters signalingParameters) {
|
| - if (peerConnectionParameters == null) {
|
| - Log.e(TAG, "Creating peer connection without initializing factory.");
|
| - return;
|
| - }
|
| - this.localRender = localRender;
|
| - this.remoteRender = remoteRender;
|
| - this.signalingParameters = signalingParameters;
|
| - executor.execute(new Runnable() {
|
| - @Override
|
| - public void run() {
|
| - createMediaConstraintsInternal();
|
| - createPeerConnectionInternal();
|
| - }
|
| - });
|
| - }
|
| -
|
| - public void close() {
|
| - executor.execute(new Runnable() {
|
| - @Override
|
| - public void run() {
|
| - closeInternal();
|
| - }
|
| - });
|
| - }
|
| -
|
| - public boolean isVideoCallEnabled() {
|
| - return videoCallEnabled;
|
| - }
|
| -
|
| - private void createPeerConnectionFactoryInternal(
|
| - Context context, EGLContext renderEGLContext) {
|
| - Log.d(TAG, "Create peer connection factory with EGLContext "
|
| - + renderEGLContext + ". Use video: "
|
| - + peerConnectionParameters.videoCallEnabled);
|
| - isError = false;
|
| - // Check if VP9 is used by default.
|
| - if (videoCallEnabled && peerConnectionParameters.videoCodec != null
|
| - && peerConnectionParameters.videoCodec.equals(VIDEO_CODEC_VP9)) {
|
| - PeerConnectionFactory.initializeFieldTrials(FIELD_TRIAL_VP9);
|
| - } else {
|
| - PeerConnectionFactory.initializeFieldTrials(null);
|
| - }
|
| - // Check if H.264 is used by default.
|
| - preferH264 = false;
|
| - if (videoCallEnabled && peerConnectionParameters.videoCodec != null
|
| - && peerConnectionParameters.videoCodec.equals(VIDEO_CODEC_H264)) {
|
| - preferH264 = true;
|
| - }
|
| - // Check if ISAC is used by default.
|
| - preferIsac = false;
|
| - if (peerConnectionParameters.audioCodec != null
|
| - && peerConnectionParameters.audioCodec.equals(AUDIO_CODEC_ISAC)) {
|
| - preferIsac = true;
|
| - }
|
| - if (!PeerConnectionFactory.initializeAndroidGlobals(
|
| - context, true, true,
|
| - peerConnectionParameters.videoCodecHwAcceleration, renderEGLContext)) {
|
| - events.onPeerConnectionError("Failed to initializeAndroidGlobals");
|
| - }
|
| - factory = new PeerConnectionFactory();
|
| - if (options != null) {
|
| - Log.d(TAG, "Factory networkIgnoreMask option: " + options.networkIgnoreMask);
|
| - factory.setOptions(options);
|
| - }
|
| - Log.d(TAG, "Peer connection factory created.");
|
| - }
|
| -
|
| - private void createMediaConstraintsInternal() {
|
| - // Create peer connection constraints.
|
| - pcConstraints = new MediaConstraints();
|
| - // Enable DTLS for normal calls and disable for loopback calls.
|
| - if (peerConnectionParameters.loopback) {
|
| - pcConstraints.optional.add(
|
| - new MediaConstraints.KeyValuePair(DTLS_SRTP_KEY_AGREEMENT_CONSTRAINT, "false"));
|
| - } else {
|
| - pcConstraints.optional.add(
|
| - new MediaConstraints.KeyValuePair(DTLS_SRTP_KEY_AGREEMENT_CONSTRAINT, "true"));
|
| - }
|
| -
|
| - // Check if there is a camera on device and disable video call if not.
|
| - numberOfCameras = VideoCapturerAndroid.getDeviceCount();
|
| - if (numberOfCameras == 0) {
|
| - Log.w(TAG, "No camera on device. Switch to audio only call.");
|
| - videoCallEnabled = false;
|
| - }
|
| - // Create video constraints if video call is enabled.
|
| - if (videoCallEnabled) {
|
| - videoConstraints = new MediaConstraints();
|
| - int videoWidth = peerConnectionParameters.videoWidth;
|
| - int videoHeight = peerConnectionParameters.videoHeight;
|
| -
|
| - // If VP8 HW video encoder is supported and video resolution is not
|
| - // specified force it to HD.
|
| - if ((videoWidth == 0 || videoHeight == 0)
|
| - && peerConnectionParameters.videoCodecHwAcceleration
|
| - && MediaCodecVideoEncoder.isVp8HwSupported()) {
|
| - videoWidth = HD_VIDEO_WIDTH;
|
| - videoHeight = HD_VIDEO_HEIGHT;
|
| - }
|
| -
|
| - // Add video resolution constraints.
|
| - if (videoWidth > 0 && videoHeight > 0) {
|
| - videoWidth = Math.min(videoWidth, MAX_VIDEO_WIDTH);
|
| - videoHeight = Math.min(videoHeight, MAX_VIDEO_HEIGHT);
|
| - videoConstraints.mandatory.add(new KeyValuePair(
|
| - MIN_VIDEO_WIDTH_CONSTRAINT, Integer.toString(videoWidth)));
|
| - videoConstraints.mandatory.add(new KeyValuePair(
|
| - MAX_VIDEO_WIDTH_CONSTRAINT, Integer.toString(videoWidth)));
|
| - videoConstraints.mandatory.add(new KeyValuePair(
|
| - MIN_VIDEO_HEIGHT_CONSTRAINT, Integer.toString(videoHeight)));
|
| - videoConstraints.mandatory.add(new KeyValuePair(
|
| - MAX_VIDEO_HEIGHT_CONSTRAINT, Integer.toString(videoHeight)));
|
| - }
|
| -
|
| - // Add fps constraints.
|
| - int videoFps = peerConnectionParameters.videoFps;
|
| - if (videoFps > 0) {
|
| - videoFps = Math.min(videoFps, MAX_VIDEO_FPS);
|
| - videoConstraints.mandatory.add(new KeyValuePair(
|
| - MIN_VIDEO_FPS_CONSTRAINT, Integer.toString(videoFps)));
|
| - videoConstraints.mandatory.add(new KeyValuePair(
|
| - MAX_VIDEO_FPS_CONSTRAINT, Integer.toString(videoFps)));
|
| - }
|
| - }
|
| -
|
| - // Create audio constraints.
|
| - audioConstraints = new MediaConstraints();
|
| - // added for audio performance measurements
|
| - if (peerConnectionParameters.noAudioProcessing) {
|
| - Log.d(TAG, "Disabling audio processing");
|
| - audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair(
|
| - AUDIO_ECHO_CANCELLATION_CONSTRAINT, "false"));
|
| - audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair(
|
| - AUDIO_AUTO_GAIN_CONTROL_CONSTRAINT, "false"));
|
| - audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair(
|
| - AUDIO_HIGH_PASS_FILTER_CONSTRAINT, "false"));
|
| - audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair(
|
| - AUDIO_NOISE_SUPPRESSION_CONSTRAINT , "false"));
|
| - }
|
| - // Create SDP constraints.
|
| - sdpMediaConstraints = new MediaConstraints();
|
| - sdpMediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair(
|
| - "OfferToReceiveAudio", "true"));
|
| - if (videoCallEnabled || peerConnectionParameters.loopback) {
|
| - sdpMediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair(
|
| - "OfferToReceiveVideo", "true"));
|
| - } else {
|
| - sdpMediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair(
|
| - "OfferToReceiveVideo", "false"));
|
| - }
|
| - }
|
| -
|
| - private void createPeerConnectionInternal() {
|
| - if (factory == null || isError) {
|
| - Log.e(TAG, "Peerconnection factory is not created");
|
| - return;
|
| - }
|
| - Log.d(TAG, "Create peer connection");
|
| - Log.d(TAG, "PCConstraints: " + pcConstraints.toString());
|
| - if (videoConstraints != null) {
|
| - Log.d(TAG, "VideoConstraints: " + videoConstraints.toString());
|
| - }
|
| - queuedRemoteCandidates = new LinkedList<IceCandidate>();
|
| -
|
| - PeerConnection.RTCConfiguration rtcConfig =
|
| - new PeerConnection.RTCConfiguration(signalingParameters.iceServers);
|
| - // TCP candidates are only useful when connecting to a server that supports
|
| - // ICE-TCP.
|
| - rtcConfig.tcpCandidatePolicy = PeerConnection.TcpCandidatePolicy.DISABLED;
|
| - rtcConfig.bundlePolicy = PeerConnection.BundlePolicy.MAXBUNDLE;
|
| - rtcConfig.rtcpMuxPolicy = PeerConnection.RtcpMuxPolicy.REQUIRE;
|
| -
|
| - peerConnection = factory.createPeerConnection(
|
| - rtcConfig, pcConstraints, pcObserver);
|
| - isInitiator = false;
|
| -
|
| - // Set default WebRTC tracing and INFO libjingle logging.
|
| - // NOTE: this _must_ happen while |factory| is alive!
|
| - Logging.enableTracing(
|
| - "logcat:",
|
| - EnumSet.of(Logging.TraceLevel.TRACE_DEFAULT),
|
| - Logging.Severity.LS_INFO);
|
| -
|
| - mediaStream = factory.createLocalMediaStream("ARDAMS");
|
| - if (videoCallEnabled) {
|
| - String cameraDeviceName = VideoCapturerAndroid.getDeviceName(0);
|
| - String frontCameraDeviceName =
|
| - VideoCapturerAndroid.getNameOfFrontFacingDevice();
|
| - if (numberOfCameras > 1 && frontCameraDeviceName != null) {
|
| - cameraDeviceName = frontCameraDeviceName;
|
| - }
|
| - Log.d(TAG, "Opening camera: " + cameraDeviceName);
|
| - videoCapturer = VideoCapturerAndroid.create(cameraDeviceName, null);
|
| - if (videoCapturer == null) {
|
| - reportError("Failed to open camera");
|
| - return;
|
| - }
|
| - mediaStream.addTrack(createVideoTrack(videoCapturer));
|
| - }
|
| -
|
| - mediaStream.addTrack(factory.createAudioTrack(
|
| - AUDIO_TRACK_ID,
|
| - factory.createAudioSource(audioConstraints)));
|
| - peerConnection.addStream(mediaStream);
|
| -
|
| - Log.d(TAG, "Peer connection created.");
|
| - }
|
| -
|
| - private void closeInternal() {
|
| - Log.d(TAG, "Closing peer connection.");
|
| - statsTimer.cancel();
|
| - if (peerConnection != null) {
|
| - peerConnection.dispose();
|
| - peerConnection = null;
|
| - }
|
| - Log.d(TAG, "Closing video source.");
|
| - if (videoSource != null) {
|
| - videoSource.dispose();
|
| - videoSource = null;
|
| - }
|
| - Log.d(TAG, "Closing peer connection factory.");
|
| - if (factory != null) {
|
| - factory.dispose();
|
| - factory = null;
|
| - }
|
| - options = null;
|
| - Log.d(TAG, "Closing peer connection done.");
|
| - events.onPeerConnectionClosed();
|
| - }
|
| -
|
| - public boolean isHDVideo() {
|
| - if (!videoCallEnabled) {
|
| - return false;
|
| - }
|
| - int minWidth = 0;
|
| - int minHeight = 0;
|
| - for (KeyValuePair keyValuePair : videoConstraints.mandatory) {
|
| - if (keyValuePair.getKey().equals("minWidth")) {
|
| - try {
|
| - minWidth = Integer.parseInt(keyValuePair.getValue());
|
| - } catch (NumberFormatException e) {
|
| - Log.e(TAG, "Can not parse video width from video constraints");
|
| - }
|
| - } else if (keyValuePair.getKey().equals("minHeight")) {
|
| - try {
|
| - minHeight = Integer.parseInt(keyValuePair.getValue());
|
| - } catch (NumberFormatException e) {
|
| - Log.e(TAG, "Can not parse video height from video constraints");
|
| - }
|
| - }
|
| - }
|
| - if (minWidth * minHeight >= 1280 * 720) {
|
| - return true;
|
| - } else {
|
| - return false;
|
| - }
|
| - }
|
| -
|
| - private void getStats() {
|
| - if (peerConnection == null || isError) {
|
| - return;
|
| - }
|
| - boolean success = peerConnection.getStats(new StatsObserver() {
|
| - @Override
|
| - public void onComplete(final StatsReport[] reports) {
|
| - events.onPeerConnectionStatsReady(reports);
|
| - }
|
| - }, null);
|
| - if (!success) {
|
| - Log.e(TAG, "getStats() returns false!");
|
| - }
|
| - }
|
| -
|
| - public void enableStatsEvents(boolean enable, int periodMs) {
|
| - if (enable) {
|
| - try {
|
| - statsTimer.schedule(new TimerTask() {
|
| - @Override
|
| - public void run() {
|
| - executor.execute(new Runnable() {
|
| - @Override
|
| - public void run() {
|
| - getStats();
|
| - }
|
| - });
|
| - }
|
| - }, 0, periodMs);
|
| - } catch (Exception e) {
|
| - Log.e(TAG, "Can not schedule statistics timer", e);
|
| - }
|
| - } else {
|
| - statsTimer.cancel();
|
| - }
|
| - }
|
| -
|
| - public void setVideoEnabled(final boolean enable) {
|
| - executor.execute(new Runnable() {
|
| - @Override
|
| - public void run() {
|
| - renderVideo = enable;
|
| - if (localVideoTrack != null) {
|
| - localVideoTrack.setEnabled(renderVideo);
|
| - }
|
| - if (remoteVideoTrack != null) {
|
| - remoteVideoTrack.setEnabled(renderVideo);
|
| - }
|
| - }
|
| - });
|
| - }
|
| -
|
| - public void createOffer() {
|
| - executor.execute(new Runnable() {
|
| - @Override
|
| - public void run() {
|
| - if (peerConnection != null && !isError) {
|
| - Log.d(TAG, "PC Create OFFER");
|
| - isInitiator = true;
|
| - peerConnection.createOffer(sdpObserver, sdpMediaConstraints);
|
| - }
|
| - }
|
| - });
|
| - }
|
| -
|
| - public void createAnswer() {
|
| - executor.execute(new Runnable() {
|
| - @Override
|
| - public void run() {
|
| - if (peerConnection != null && !isError) {
|
| - Log.d(TAG, "PC create ANSWER");
|
| - isInitiator = false;
|
| - peerConnection.createAnswer(sdpObserver, sdpMediaConstraints);
|
| - }
|
| - }
|
| - });
|
| - }
|
| -
|
| - public void addRemoteIceCandidate(final IceCandidate candidate) {
|
| - executor.execute(new Runnable() {
|
| - @Override
|
| - public void run() {
|
| - if (peerConnection != null && !isError) {
|
| - if (queuedRemoteCandidates != null) {
|
| - queuedRemoteCandidates.add(candidate);
|
| - } else {
|
| - peerConnection.addIceCandidate(candidate);
|
| - }
|
| - }
|
| - }
|
| - });
|
| - }
|
| -
|
| - public void setRemoteDescription(final SessionDescription sdp) {
|
| - executor.execute(new Runnable() {
|
| - @Override
|
| - public void run() {
|
| - if (peerConnection == null || isError) {
|
| - return;
|
| - }
|
| - String sdpDescription = sdp.description;
|
| - if (preferIsac) {
|
| - sdpDescription = preferCodec(sdpDescription, AUDIO_CODEC_ISAC, true);
|
| - }
|
| - if (videoCallEnabled && preferH264) {
|
| - sdpDescription = preferCodec(sdpDescription, VIDEO_CODEC_H264, false);
|
| - }
|
| - if (videoCallEnabled && peerConnectionParameters.videoStartBitrate > 0) {
|
| - sdpDescription = setStartBitrate(VIDEO_CODEC_VP8, true,
|
| - sdpDescription, peerConnectionParameters.videoStartBitrate);
|
| - sdpDescription = setStartBitrate(VIDEO_CODEC_VP9, true,
|
| - sdpDescription, peerConnectionParameters.videoStartBitrate);
|
| - sdpDescription = setStartBitrate(VIDEO_CODEC_H264, true,
|
| - sdpDescription, peerConnectionParameters.videoStartBitrate);
|
| - }
|
| - if (peerConnectionParameters.audioStartBitrate > 0) {
|
| - sdpDescription = setStartBitrate(AUDIO_CODEC_OPUS, false,
|
| - sdpDescription, peerConnectionParameters.audioStartBitrate);
|
| - }
|
| - Log.d(TAG, "Set remote SDP.");
|
| - SessionDescription sdpRemote = new SessionDescription(
|
| - sdp.type, sdpDescription);
|
| - peerConnection.setRemoteDescription(sdpObserver, sdpRemote);
|
| - }
|
| - });
|
| - }
|
| -
|
| - public void stopVideoSource() {
|
| - executor.execute(new Runnable() {
|
| - @Override
|
| - public void run() {
|
| - if (videoSource != null && !videoSourceStopped) {
|
| - Log.d(TAG, "Stop video source.");
|
| - videoSource.stop();
|
| - videoSourceStopped = true;
|
| - }
|
| - }
|
| - });
|
| - }
|
| -
|
| - public void startVideoSource() {
|
| - executor.execute(new Runnable() {
|
| - @Override
|
| - public void run() {
|
| - if (videoSource != null && videoSourceStopped) {
|
| - Log.d(TAG, "Restart video source.");
|
| - videoSource.restart();
|
| - videoSourceStopped = false;
|
| - }
|
| - }
|
| - });
|
| - }
|
| -
|
| - private void reportError(final String errorMessage) {
|
| - Log.e(TAG, "Peerconnection error: " + errorMessage);
|
| - executor.execute(new Runnable() {
|
| - @Override
|
| - public void run() {
|
| - if (!isError) {
|
| - events.onPeerConnectionError(errorMessage);
|
| - isError = true;
|
| - }
|
| - }
|
| - });
|
| - }
|
| -
|
| - private VideoTrack createVideoTrack(VideoCapturerAndroid capturer) {
|
| - videoSource = factory.createVideoSource(capturer, videoConstraints);
|
| -
|
| - localVideoTrack = factory.createVideoTrack(VIDEO_TRACK_ID, videoSource);
|
| - localVideoTrack.setEnabled(renderVideo);
|
| - localVideoTrack.addRenderer(new VideoRenderer(localRender));
|
| - return localVideoTrack;
|
| - }
|
| -
|
| - private static String setStartBitrate(String codec, boolean isVideoCodec,
|
| - String sdpDescription, int bitrateKbps) {
|
| - String[] lines = sdpDescription.split("\r\n");
|
| - int rtpmapLineIndex = -1;
|
| - boolean sdpFormatUpdated = false;
|
| - String codecRtpMap = null;
|
| - // Search for codec rtpmap in format
|
| - // a=rtpmap:<payload type> <encoding name>/<clock rate> [/<encoding parameters>]
|
| - String regex = "^a=rtpmap:(\\d+) " + codec + "(/\\d+)+[\r]?$";
|
| - Pattern codecPattern = Pattern.compile(regex);
|
| - for (int i = 0; i < lines.length; i++) {
|
| - Matcher codecMatcher = codecPattern.matcher(lines[i]);
|
| - if (codecMatcher.matches()) {
|
| - codecRtpMap = codecMatcher.group(1);
|
| - rtpmapLineIndex = i;
|
| - break;
|
| - }
|
| - }
|
| - if (codecRtpMap == null) {
|
| - Log.w(TAG, "No rtpmap for " + codec + " codec");
|
| - return sdpDescription;
|
| - }
|
| - Log.d(TAG, "Found " + codec + " rtpmap " + codecRtpMap
|
| - + " at " + lines[rtpmapLineIndex]);
|
| -
|
| - // Check if a=fmtp string already exist in remote SDP for this codec and
|
| - // update it with new bitrate parameter.
|
| - regex = "^a=fmtp:" + codecRtpMap + " \\w+=\\d+.*[\r]?$";
|
| - codecPattern = Pattern.compile(regex);
|
| - for (int i = 0; i < lines.length; i++) {
|
| - Matcher codecMatcher = codecPattern.matcher(lines[i]);
|
| - if (codecMatcher.matches()) {
|
| - Log.d(TAG, "Found " + codec + " " + lines[i]);
|
| - if (isVideoCodec) {
|
| - lines[i] += "; " + VIDEO_CODEC_PARAM_START_BITRATE
|
| - + "=" + bitrateKbps;
|
| - } else {
|
| - lines[i] += "; " + AUDIO_CODEC_PARAM_BITRATE
|
| - + "=" + (bitrateKbps * 1000);
|
| - }
|
| - Log.d(TAG, "Update remote SDP line: " + lines[i]);
|
| - sdpFormatUpdated = true;
|
| - break;
|
| - }
|
| - }
|
| -
|
| - StringBuilder newSdpDescription = new StringBuilder();
|
| - for (int i = 0; i < lines.length; i++) {
|
| - newSdpDescription.append(lines[i]).append("\r\n");
|
| - // Append new a=fmtp line if no such line exist for a codec.
|
| - if (!sdpFormatUpdated && i == rtpmapLineIndex) {
|
| - String bitrateSet;
|
| - if (isVideoCodec) {
|
| - bitrateSet = "a=fmtp:" + codecRtpMap + " "
|
| - + VIDEO_CODEC_PARAM_START_BITRATE + "=" + bitrateKbps;
|
| - } else {
|
| - bitrateSet = "a=fmtp:" + codecRtpMap + " "
|
| - + AUDIO_CODEC_PARAM_BITRATE + "=" + (bitrateKbps * 1000);
|
| - }
|
| - Log.d(TAG, "Add remote SDP line: " + bitrateSet);
|
| - newSdpDescription.append(bitrateSet).append("\r\n");
|
| - }
|
| -
|
| - }
|
| - return newSdpDescription.toString();
|
| - }
|
| -
|
| - private static String preferCodec(
|
| - String sdpDescription, String codec, boolean isAudio) {
|
| - String[] lines = sdpDescription.split("\r\n");
|
| - int mLineIndex = -1;
|
| - String codecRtpMap = null;
|
| - // a=rtpmap:<payload type> <encoding name>/<clock rate> [/<encoding parameters>]
|
| - String regex = "^a=rtpmap:(\\d+) " + codec + "(/\\d+)+[\r]?$";
|
| - Pattern codecPattern = Pattern.compile(regex);
|
| - String mediaDescription = "m=video ";
|
| - if (isAudio) {
|
| - mediaDescription = "m=audio ";
|
| - }
|
| - for (int i = 0; (i < lines.length)
|
| - && (mLineIndex == -1 || codecRtpMap == null); i++) {
|
| - if (lines[i].startsWith(mediaDescription)) {
|
| - mLineIndex = i;
|
| - continue;
|
| - }
|
| - Matcher codecMatcher = codecPattern.matcher(lines[i]);
|
| - if (codecMatcher.matches()) {
|
| - codecRtpMap = codecMatcher.group(1);
|
| - continue;
|
| - }
|
| - }
|
| - if (mLineIndex == -1) {
|
| - Log.w(TAG, "No " + mediaDescription + " line, so can't prefer " + codec);
|
| - return sdpDescription;
|
| - }
|
| - if (codecRtpMap == null) {
|
| - Log.w(TAG, "No rtpmap for " + codec);
|
| - return sdpDescription;
|
| - }
|
| - Log.d(TAG, "Found " + codec + " rtpmap " + codecRtpMap + ", prefer at "
|
| - + lines[mLineIndex]);
|
| - String[] origMLineParts = lines[mLineIndex].split(" ");
|
| - if (origMLineParts.length > 3) {
|
| - StringBuilder newMLine = new StringBuilder();
|
| - int origPartIndex = 0;
|
| - // Format is: m=<media> <port> <proto> <fmt> ...
|
| - newMLine.append(origMLineParts[origPartIndex++]).append(" ");
|
| - newMLine.append(origMLineParts[origPartIndex++]).append(" ");
|
| - newMLine.append(origMLineParts[origPartIndex++]).append(" ");
|
| - newMLine.append(codecRtpMap);
|
| - for (; origPartIndex < origMLineParts.length; origPartIndex++) {
|
| - if (!origMLineParts[origPartIndex].equals(codecRtpMap)) {
|
| - newMLine.append(" ").append(origMLineParts[origPartIndex]);
|
| - }
|
| - }
|
| - lines[mLineIndex] = newMLine.toString();
|
| - Log.d(TAG, "Change media description: " + lines[mLineIndex]);
|
| - } else {
|
| - Log.e(TAG, "Wrong SDP media description format: " + lines[mLineIndex]);
|
| - }
|
| - StringBuilder newSdpDescription = new StringBuilder();
|
| - for (String line : lines) {
|
| - newSdpDescription.append(line).append("\r\n");
|
| - }
|
| - return newSdpDescription.toString();
|
| - }
|
| -
|
| - private void drainCandidates() {
|
| - if (queuedRemoteCandidates != null) {
|
| - Log.d(TAG, "Add " + queuedRemoteCandidates.size() + " remote candidates");
|
| - for (IceCandidate candidate : queuedRemoteCandidates) {
|
| - peerConnection.addIceCandidate(candidate);
|
| - }
|
| - queuedRemoteCandidates = null;
|
| - }
|
| - }
|
| -
|
| - private void switchCameraInternal() {
|
| - if (!videoCallEnabled || numberOfCameras < 2 || isError || videoCapturer == null) {
|
| - Log.e(TAG, "Failed to switch camera. Video: " + videoCallEnabled + ". Error : "
|
| - + isError + ". Number of cameras: " + numberOfCameras);
|
| - return; // No video is sent or only one camera is available or error happened.
|
| - }
|
| - Log.d(TAG, "Switch camera");
|
| - videoCapturer.switchCamera(null);
|
| - }
|
| -
|
| - public void switchCamera() {
|
| - executor.execute(new Runnable() {
|
| - @Override
|
| - public void run() {
|
| - switchCameraInternal();
|
| - }
|
| - });
|
| - }
|
| -
|
| - // Implementation detail: observe ICE & stream changes and react accordingly.
|
| - private class PCObserver implements PeerConnection.Observer {
|
| - @Override
|
| - public void onIceCandidate(final IceCandidate candidate){
|
| - executor.execute(new Runnable() {
|
| - @Override
|
| - public void run() {
|
| - events.onIceCandidate(candidate);
|
| - }
|
| - });
|
| - }
|
| -
|
| - @Override
|
| - public void onSignalingChange(
|
| - PeerConnection.SignalingState newState) {
|
| - Log.d(TAG, "SignalingState: " + newState);
|
| - }
|
| -
|
| - @Override
|
| - public void onIceConnectionChange(
|
| - final PeerConnection.IceConnectionState newState) {
|
| - executor.execute(new Runnable() {
|
| - @Override
|
| - public void run() {
|
| - Log.d(TAG, "IceConnectionState: " + newState);
|
| - if (newState == IceConnectionState.CONNECTED) {
|
| - events.onIceConnected();
|
| - } else if (newState == IceConnectionState.DISCONNECTED) {
|
| - events.onIceDisconnected();
|
| - } else if (newState == IceConnectionState.FAILED) {
|
| - reportError("ICE connection failed.");
|
| - }
|
| - }
|
| - });
|
| - }
|
| -
|
| - @Override
|
| - public void onIceGatheringChange(
|
| - PeerConnection.IceGatheringState newState) {
|
| - Log.d(TAG, "IceGatheringState: " + newState);
|
| - }
|
| -
|
| - @Override
|
| - public void onIceConnectionReceivingChange(boolean receiving) {
|
| - Log.d(TAG, "IceConnectionReceiving changed to " + receiving);
|
| - }
|
| -
|
| - @Override
|
| - public void onAddStream(final MediaStream stream){
|
| - executor.execute(new Runnable() {
|
| - @Override
|
| - public void run() {
|
| - if (peerConnection == null || isError) {
|
| - return;
|
| - }
|
| - if (stream.audioTracks.size() > 1 || stream.videoTracks.size() > 1) {
|
| - reportError("Weird-looking stream: " + stream);
|
| - return;
|
| - }
|
| - if (stream.videoTracks.size() == 1) {
|
| - remoteVideoTrack = stream.videoTracks.get(0);
|
| - remoteVideoTrack.setEnabled(renderVideo);
|
| - remoteVideoTrack.addRenderer(new VideoRenderer(remoteRender));
|
| - }
|
| - }
|
| - });
|
| - }
|
| -
|
| - @Override
|
| - public void onRemoveStream(final MediaStream stream){
|
| - executor.execute(new Runnable() {
|
| - @Override
|
| - public void run() {
|
| - if (peerConnection == null || isError) {
|
| - return;
|
| - }
|
| - remoteVideoTrack = null;
|
| - stream.videoTracks.get(0).dispose();
|
| - }
|
| - });
|
| - }
|
| -
|
| - @Override
|
| - public void onDataChannel(final DataChannel dc) {
|
| - reportError("AppRTC doesn't use data channels, but got: " + dc.label()
|
| - + " anyway!");
|
| - }
|
| -
|
| - @Override
|
| - public void onRenegotiationNeeded() {
|
| - // No need to do anything; AppRTC follows a pre-agreed-upon
|
| - // signaling/negotiation protocol.
|
| - }
|
| - }
|
| -
|
| - // Implementation detail: handle offer creation/signaling and answer setting,
|
| - // as well as adding remote ICE candidates once the answer SDP is set.
|
| - private class SDPObserver implements SdpObserver {
|
| - @Override
|
| - public void onCreateSuccess(final SessionDescription origSdp) {
|
| - if (localSdp != null) {
|
| - reportError("Multiple SDP create.");
|
| - return;
|
| - }
|
| - String sdpDescription = origSdp.description;
|
| - if (preferIsac) {
|
| - sdpDescription = preferCodec(sdpDescription, AUDIO_CODEC_ISAC, true);
|
| - }
|
| - if (videoCallEnabled && preferH264) {
|
| - sdpDescription = preferCodec(sdpDescription, VIDEO_CODEC_H264, false);
|
| - }
|
| - final SessionDescription sdp = new SessionDescription(
|
| - origSdp.type, sdpDescription);
|
| - localSdp = sdp;
|
| - executor.execute(new Runnable() {
|
| - @Override
|
| - public void run() {
|
| - if (peerConnection != null && !isError) {
|
| - Log.d(TAG, "Set local SDP from " + sdp.type);
|
| - peerConnection.setLocalDescription(sdpObserver, sdp);
|
| - }
|
| - }
|
| - });
|
| - }
|
| -
|
| - @Override
|
| - public void onSetSuccess() {
|
| - executor.execute(new Runnable() {
|
| - @Override
|
| - public void run() {
|
| - if (peerConnection == null || isError) {
|
| - return;
|
| - }
|
| - if (isInitiator) {
|
| - // For offering peer connection we first create offer and set
|
| - // local SDP, then after receiving answer set remote SDP.
|
| - if (peerConnection.getRemoteDescription() == null) {
|
| - // We've just set our local SDP so time to send it.
|
| - Log.d(TAG, "Local SDP set succesfully");
|
| - events.onLocalDescription(localSdp);
|
| - } else {
|
| - // We've just set remote description, so drain remote
|
| - // and send local ICE candidates.
|
| - Log.d(TAG, "Remote SDP set succesfully");
|
| - drainCandidates();
|
| - }
|
| - } else {
|
| - // For answering peer connection we set remote SDP and then
|
| - // create answer and set local SDP.
|
| - if (peerConnection.getLocalDescription() != null) {
|
| - // We've just set our local SDP so time to send it, drain
|
| - // remote and send local ICE candidates.
|
| - Log.d(TAG, "Local SDP set succesfully");
|
| - events.onLocalDescription(localSdp);
|
| - drainCandidates();
|
| - } else {
|
| - // We've just set remote SDP - do nothing for now -
|
| - // answer will be created soon.
|
| - Log.d(TAG, "Remote SDP set succesfully");
|
| - }
|
| - }
|
| - }
|
| - });
|
| - }
|
| -
|
| - @Override
|
| - public void onCreateFailure(final String error) {
|
| - reportError("createSDP error: " + error);
|
| - }
|
| -
|
| - @Override
|
| - public void onSetFailure(final String error) {
|
| - reportError("setSDP error: " + error);
|
| - }
|
| - }
|
| -}
|
|
|