/*
* Copyright 2015 OpenMarket Ltd
* Copyright 2017 Vector Creations Ltd
*
* 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 org.matrix.androidsdk.call;
import android.content.Context;
import android.hardware.Camera;
import android.opengl.GLSurfaceView;
import android.os.Build;
import android.text.TextUtils;
import org.matrix.androidsdk.util.Log;
import android.view.Surface;
import android.view.View;
import android.view.WindowManager;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import org.matrix.androidsdk.MXSession;
import org.matrix.androidsdk.rest.model.Event;
import org.webrtc.AudioSource;
import org.webrtc.AudioTrack;
import org.webrtc.DataChannel;
import org.webrtc.IceCandidate;
import org.webrtc.MediaConstraints;
import org.webrtc.MediaStream;
import org.webrtc.PeerConnection;
import org.webrtc.PeerConnectionFactory;
import org.webrtc.SdpObserver;
import org.webrtc.SessionDescription;
import org.webrtc.VideoCapturer;
import org.webrtc.VideoCapturerAndroid;
import org.webrtc.VideoRenderer;
import org.webrtc.VideoRendererGui;
import org.webrtc.VideoSource;
import org.webrtc.VideoTrack;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Timer;
import java.util.TimerTask;
public class MXJingleCall extends MXCall {
private static final String LOG_TAG = "MXJingleCall";
private static final String VIDEO_TRACK_ID = "ARDAMSv0";
private static final String AUDIO_TRACK_ID = "ARDAMSa0";
private static final String MIN_VIDEO_WIDTH_CONSTRAINT = "minWidth";
private static final int MIN_VIDEO_WIDTH = 640;
private static final int CAMERA_TYPE_FRONT = 1;
private static final int CAMERA_TYPE_REAR = 2;
private static final int CAMERA_TYPE_UNDEFINED = -1;
static private PeerConnectionFactory mPeerConnectionFactory = null;
static private String mFrontCameraName = null;
static private String mBackCameraName = null;
static private VideoCapturer mVideoCapturer = null;
private GLSurfaceView mCallView = null;
private boolean mIsCameraSwitched;
private boolean mIsVideoSourceStopped = false;
private VideoSource mVideoSource = null;
private VideoTrack mLocalVideoTrack = null;
private AudioSource mAudioSource = null;
private AudioTrack mLocalAudioTrack = null;
private MediaStream mLocalMediaStream = null;
private VideoTrack mRemoteVideoTrack = null;
private PeerConnection mPeerConnection = null;
// default value
private String mCallState = CALL_STATE_CREATED;
private boolean mUsingLargeLocalRenderer = true;
private VideoRenderer mLargeRemoteRenderer = null;
private VideoRenderer mSmallLocalRenderer = null;
private static int mLocalRenderWidth = -1;
private static int mLocalRenderHeight = -1;
private VideoRenderer.Callbacks mLargeLocalRendererCallbacks = null;
private VideoRenderer.Callbacks mSmallLocalRendererCallbacks;
private VideoRenderer mLargeLocalRenderer = null;
private static boolean mIsInitialized = false;
// null -> not initialized
// true / false for the supported status
private static Boolean mIsSupported;
// candidate management
private boolean mIsIncomingPrepared = false;
private JsonArray mPendingCandidates = new JsonArray();
private JsonObject mCallInviteParams = null;
private int mCameraInUse = CAMERA_TYPE_UNDEFINED;
private boolean mIsAnswered = false;
/**
* @return true if this stack can perform calls.
*/
public static boolean isSupported(Context context) {
if (null == mIsSupported) {
mIsSupported = Build.VERSION.SDK_INT > Build.VERSION_CODES.ICE_CREAM_SANDWICH;
// the call initialisation is not yet done
if (mIsSupported) {
initializeAndroidGlobals(context.getApplicationContext());
}
Log.d(LOG_TAG, "isSupported " + mIsSupported);
}
return mIsSupported;
}
/**
* Class creator
*
* @param session the session
* @param context the context
* @param turnServer the turn server
*/
public MXJingleCall(MXSession session, Context context, JsonElement turnServer) {
if (!isSupported(context)) {
throw new AssertionError("MXJingleCall : not supported with the current android version");
}
if (null == session) {
throw new AssertionError("MXJingleCall : session cannot be null");
}
if (null == context) {
throw new AssertionError("MXJingleCall : context cannot be null");
}
Log.d(LOG_TAG, "MXJingleCall constructor " + turnServer);
mCallId = "c" + System.currentTimeMillis();
mSession = session;
mContext = context;
mTurnServer = turnServer;
}
/**
* Initialize the jingle globals
*/
private static void initializeAndroidGlobals(Context context) {
if (!mIsInitialized) {
try {
mIsInitialized = PeerConnectionFactory.initializeAndroidGlobals(
context,
true, // enable audio initializing
true, // enable video initializing
true, // enable hardware acceleration
VideoRendererGui.getEGLContext());
PeerConnectionFactory.initializeFieldTrials(null);
mIsSupported = true;
Log.d(LOG_TAG, "## initializeAndroidGlobals(): mIsInitialized=" + mIsInitialized);
} catch (UnsatisfiedLinkError e) {
Log.e(LOG_TAG, "## initializeAndroidGlobals(): Exception Msg=" + e.getMessage());
mIsInitialized = true;
mIsSupported = false;
} catch (Exception e) {
Log.e(LOG_TAG, "## initializeAndroidGlobals(): Exception Msg=" + e.getMessage());
mIsInitialized = true;
mIsSupported = false;
}
}
}
/**
* Create the callviews
*/
@Override
public void createCallView() {
if ((null != mIsSupported) && mIsSupported) {
Log.d(LOG_TAG, "MXJingleCall createCallView");
dispatchOnStateDidChange(CALL_STATE_CREATING_CALL_VIEW);
mUIThreadHandler.postDelayed(new Runnable() {
@Override
public void run() {
mCallView = new GLSurfaceView(mContext); // set the GLSurfaceView where it should render to
mCallView.setVisibility(View.GONE);
dispatchOnViewLoading(mCallView);
mUIThreadHandler.post(new Runnable() {
@Override
public void run() {
dispatchOnStateDidChange(CALL_STATE_FLEDGLING);
dispatchOnViewReady();
}
});
}
}, 10);
}
}
/**
* The connection is terminated
*
* @param endCallReasonId the reason of the call ending
*/
private void terminate(final int endCallReasonId) {
Log.d(LOG_TAG, "## terminate(): reason= " + endCallReasonId);
if (isCallEnded()) {
return;
}
dispatchOnStateDidChange(CALL_STATE_ENDED);
boolean isPeerConnectionFactoryAllowed = false;
if (null != mPeerConnection) {
mPeerConnection.dispose();
mPeerConnection = null;
// the call has been initialized so mPeerConnectionFactory can be released
isPeerConnectionFactoryAllowed = true;
}
if (null != mVideoSource) {
mVideoSource.dispose();
mVideoSource = null;
}
if (null != mAudioSource) {
mAudioSource.dispose();
mAudioSource = null;
}
// mPeerConnectionFactory is static so it might be used by another call
// so we test that the current has been created
if (isPeerConnectionFactoryAllowed && (null != mPeerConnectionFactory)) {
mPeerConnectionFactory.dispose();
mPeerConnectionFactory = null;
}
if (null != mCallView) {
final View fCallView = mCallView;
fCallView.post(new Runnable() {
@Override
public void run() {
fCallView.setVisibility(View.GONE);
}
});
mCallView = null;
}
mUIThreadHandler.post(new Runnable() {
@Override
public void run() {
dispatchOnCallEnd(endCallReasonId);
}
});
}
/**
* Send the invite event
*
* @param sessionDescription the session description.
*/
private void sendInvite(final SessionDescription sessionDescription) {
// check if the call has not been killed
if (isCallEnded()) {
Log.d(LOG_TAG, "MXJingleCall isCallEnded");
return;
}
Log.d(LOG_TAG, "MXJingleCall sendInvite");
// build the invitation event
JsonObject inviteContent = new JsonObject();
inviteContent.addProperty("version", 0);
inviteContent.addProperty("call_id", mCallId);
inviteContent.addProperty("lifetime", 60000);
JsonObject offerContent = new JsonObject();
offerContent.addProperty("sdp", sessionDescription.description);
offerContent.addProperty("type", sessionDescription.type.canonicalForm());
inviteContent.add("offer", offerContent);
Event event = new Event(Event.EVENT_TYPE_CALL_INVITE, inviteContent, mSession.getCredentials().userId, mCallSignalingRoom.getRoomId());
mPendingEvents.add(event);
mCallTimeoutTimer = new Timer();
mCallTimeoutTimer.schedule(new TimerTask() {
@Override
public void run() {
try {
if (getCallState().equals(IMXCall.CALL_STATE_RINGING) || getCallState().equals(IMXCall.CALL_STATE_INVITE_SENT)) {
Log.d(LOG_TAG, "sendInvite : CALL_ERROR_USER_NOT_RESPONDING");
dispatchOnCallError(CALL_ERROR_USER_NOT_RESPONDING);
hangup(null);
}
// cancel the timer
mCallTimeoutTimer.cancel();
mCallTimeoutTimer = null;
} catch (Exception e) {
Log.e(LOG_TAG, "## sendInvite(): Exception Msg= " + e.getMessage());
}
}
}, 60 * 1000);
sendNextEvent();
}
/**
* Send the answer event
*
* @param sessionDescription the session description
*/
private void sendAnswer(final SessionDescription sessionDescription) {
// check if the call has not been killed
if (isCallEnded()) {
Log.d(LOG_TAG, "sendAnswer isCallEnded");
return;
}
Log.d(LOG_TAG, "sendAnswer");
// build the invitation event
JsonObject answerContent = new JsonObject();
answerContent.addProperty("version", 0);
answerContent.addProperty("call_id", mCallId);
answerContent.addProperty("lifetime", 60000);
JsonObject offerContent = new JsonObject();
offerContent.addProperty("sdp", sessionDescription.description);
offerContent.addProperty("type", sessionDescription.type.canonicalForm());
answerContent.add("answer", offerContent);
Event event = new Event(Event.EVENT_TYPE_CALL_ANSWER, answerContent, mSession.getCredentials().userId, mCallSignalingRoom.getRoomId());
mPendingEvents.add(event);
sendNextEvent();
mIsAnswered = true;
}
@Override
public void updateLocalVideoRendererPosition(VideoLayoutConfiguration aConfigurationToApply) {
try {
// compute the new layout
if ((null != mSmallLocalRendererCallbacks) && (null != aConfigurationToApply)) {
VideoRendererGui.update(mSmallLocalRendererCallbacks, aConfigurationToApply.mX, aConfigurationToApply.mY, aConfigurationToApply.mWidth, aConfigurationToApply.mHeight, VideoRendererGui.ScalingType.SCALE_ASPECT_FIT, true);
Log.d(LOG_TAG, "## updateLocalVideoRendererPosition(): X=" + aConfigurationToApply.mX + " Y=" + aConfigurationToApply.mY + " width=" + aConfigurationToApply.mWidth + " height" + aConfigurationToApply.mHeight);
} else {
Log.w(LOG_TAG, "## updateLocalVideoRendererPosition(): Skipped due to invalid parameters");
}
} catch (Exception e) {
Log.e(LOG_TAG, "## updateLocalVideoRendererPosition(): Exception Msg=" + e.getMessage());
return;
}
if (null != mCallView) {
mCallView.postInvalidate();
} else {
Log.w(LOG_TAG, "## updateLocalVideoRendererPosition(): Skipped due to mCallView = null");
}
}
@Override
public boolean isSwitchCameraSupported() {
return (VideoCapturerAndroid.getDeviceCount() > 1);
}
@Override
public boolean switchRearFrontCamera() {
if ((null != mVideoCapturer) && (isSwitchCameraSupported())) {
VideoCapturerAndroid videoCapturerAndroid = (VideoCapturerAndroid) mVideoCapturer;
if (videoCapturerAndroid.switchCamera(null)) {
// toggle the video capturer instance
if (CAMERA_TYPE_FRONT == mCameraInUse) {
mCameraInUse = CAMERA_TYPE_REAR;
} else {
mCameraInUse = CAMERA_TYPE_FRONT;
}
// compute camera switch new status
mIsCameraSwitched = !mIsCameraSwitched;
mUIThreadHandler.postDelayed(new Runnable() {
@Override
public void run() {
listenPreviewUpdate();
}
}, 500);
return true;
} else {
Log.w(LOG_TAG, "## switchRearFrontCamera(): failed");
}
} else {
Log.w(LOG_TAG, "## switchRearFrontCamera(): failure - invalid values");
}
return false;
}
@Override
public void muteVideoRecording(boolean muteValue) {
Log.d(LOG_TAG, "## muteVideoRecording(): muteValue=" + muteValue);
if (!isCallEnded()) {
if (null != mLocalVideoTrack) {
mLocalVideoTrack.setEnabled(!muteValue);
} else {
Log.d(LOG_TAG, "## muteVideoRecording(): failure - invalid value");
}
} else {
Log.d(LOG_TAG, "## muteVideoRecording(): the call is ended");
}
}
@Override
public boolean isVideoRecordingMuted() {
boolean isMuted = false;
if (!isCallEnded()) {
if (null != mLocalVideoTrack) {
isMuted = !mLocalVideoTrack.enabled();
} else {
Log.w(LOG_TAG, "## isVideoRecordingMuted(): failure - invalid value");
}
Log.d(LOG_TAG, "## isVideoRecordingMuted() = " + isMuted);
} else {
Log.d(LOG_TAG, "## isVideoRecordingMuted() : the call is ended");
}
return isMuted;
}
@Override
public boolean isCameraSwitched() {
return mIsCameraSwitched;
}
@Override
public void addListener(MXCallListener callListener) {
super.addListener(callListener);
// warn about the preview update
if ((-1 != mLocalRenderWidth) && (1 != mLocalRenderHeight)) {
mUIThreadHandler.post(new Runnable() {
@Override
public void run() {
dispatchOnPreviewSizeChanged(mLocalRenderWidth, mLocalRenderHeight);
}
});
}
}
/**
* create the local stream
*/
private void createLocalStream() {
Log.d(LOG_TAG, "## createLocalStream(): IN");
// check there is at least one stream to start a call
if ((null == mLocalVideoTrack) && (null == mLocalAudioTrack)) {
Log.d(LOG_TAG, "## createLocalStream(): CALL_ERROR_CALL_INIT_FAILED");
dispatchOnCallError(CALL_ERROR_CALL_INIT_FAILED);
hangup("no_stream");
terminate(IMXCall.END_CALL_REASON_UNDEFINED);
return;
}
// create our local stream to add our audio and video tracks
mLocalMediaStream = mPeerConnectionFactory.createLocalMediaStream("ARDAMS");
// add video track to local stream
if (null != mLocalVideoTrack) {
mLocalMediaStream.addTrack(mLocalVideoTrack);
}
// add audio track to local stream
if (null != mLocalAudioTrack) {
mLocalMediaStream.addTrack(mLocalAudioTrack);
}
// build ICE servers list
ArrayList<PeerConnection.IceServer> iceServers = new ArrayList<>();
if (null != mTurnServer) {
try {
String username = null;
String password = null;
JsonObject object = mTurnServer.getAsJsonObject();
if (object.has("username")) {
username = object.get("username").getAsString();
}
if (object.has("password")) {
password = object.get("password").getAsString();
}
JsonArray uris = object.get("uris").getAsJsonArray();
for (int index = 0; index < uris.size(); index++) {
String url = uris.get(index).getAsString();
if ((null != username) && (null != password)) {
iceServers.add(new PeerConnection.IceServer(url, username, password));
} else {
iceServers.add(new PeerConnection.IceServer(url));
}
}
} catch (Exception e) {
Log.e(LOG_TAG, "## createLocalStream(): Exception in ICE servers list Msg=" + e.getLocalizedMessage());
}
}
// define at least on server
if (iceServers.size() == 0) {
iceServers.add(new PeerConnection.IceServer("stun:stun.l.google.com:19302"));
}
// define constraints
MediaConstraints pcConstraints = new MediaConstraints();
pcConstraints.optional.add(new MediaConstraints.KeyValuePair("RtpDataChannels", "true"));
// start connecting to the other peer by creating the peer connection
mPeerConnection = mPeerConnectionFactory.createPeerConnection(
iceServers,
pcConstraints,
new PeerConnection.Observer() {
@Override
public void onSignalingChange(PeerConnection.SignalingState signalingState) {
Log.d(LOG_TAG, "## mPeerConnection creation: onSignalingChange state=" + signalingState);
}
@Override
public void onIceConnectionChange(final PeerConnection.IceConnectionState iceConnectionState) {
Log.d(LOG_TAG, "## mPeerConnection creation: onIceConnectionChange " + iceConnectionState);
mUIThreadHandler.post(new Runnable() {
@Override
public void run() {
if (iceConnectionState == PeerConnection.IceConnectionState.CONNECTED) {
if ((null != mLocalVideoTrack) && mUsingLargeLocalRenderer && isVideo()) {
mLocalVideoTrack.setEnabled(false);
VideoRendererGui.remove(mLargeLocalRendererCallbacks);
mLocalVideoTrack.removeRenderer(mLargeLocalRenderer);
// in conference call, there is no local preview,
// the local attendee video is sent by the server among the others conference attendees.
if (!isConference()) {
// add local preview, only for 1:1 call
mLocalVideoTrack.addRenderer(mSmallLocalRenderer);
}
listenPreviewUpdate();
mLocalVideoTrack.setEnabled(true);
mUsingLargeLocalRenderer = false;
mCallView.post(new Runnable() {
@Override
public void run() {
if (null != mCallView) {
mCallView.invalidate();
}
}
});
}
dispatchOnStateDidChange(IMXCall.CALL_STATE_CONNECTED);
}
// theses states are ignored
// only the matrix hangup event is managed
/*else if (iceConnectionState == PeerConnection.IceConnectionState.DISCONNECTED) {
// TODO warn the user ?
hangup(null);
} else if (iceConnectionState == PeerConnection.IceConnectionState.CLOSED) {
// TODO warn the user ?
terminate();
}*/
else if (iceConnectionState == PeerConnection.IceConnectionState.FAILED) {
dispatchOnCallError(CALL_ERROR_ICE_FAILED);
hangup("ice_failed");
}
}
});
}
@Override
public void onIceConnectionReceivingChange(boolean var1) {
Log.d(LOG_TAG, "## mPeerConnection creation: onIceConnectionReceivingChange " + var1);
}
@Override
public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) {
Log.d(LOG_TAG, "## mPeerConnection creation: onIceGatheringChange " + iceGatheringState);
}
@Override
public void onIceCandidate(final IceCandidate iceCandidate) {
Log.d(LOG_TAG, "## mPeerConnection creation: onIceCandidate " + iceCandidate);
mUIThreadHandler.post(new Runnable() {
@Override
public void run() {
if (!isCallEnded()) {
JsonObject content = new JsonObject();
content.addProperty("version", 0);
content.addProperty("call_id", mCallId);
JsonArray candidates = new JsonArray();
JsonObject cand = new JsonObject();
cand.addProperty("sdpMLineIndex", iceCandidate.sdpMLineIndex);
cand.addProperty("sdpMid", iceCandidate.sdpMid);
cand.addProperty("candidate", iceCandidate.sdp);
candidates.add(cand);
content.add("candidates", candidates);
boolean addIt = true;
// merge candidates
if (mPendingEvents.size() > 0) {
try {
Event lastEvent = mPendingEvents.get(mPendingEvents.size() - 1);
if (TextUtils.equals(lastEvent.getType(), Event.EVENT_TYPE_CALL_CANDIDATES)) {
// return the content cast as a JsonObject
// it is not a copy
JsonObject lastContent = lastEvent.getContentAsJsonObject();
JsonArray lastContentCandidates = lastContent.get("candidates").getAsJsonArray();
JsonArray newContentCandidates = content.get("candidates").getAsJsonArray();
Log.d(LOG_TAG, "Merge candidates from " + lastContentCandidates.size() + " to " + (lastContentCandidates.size() + newContentCandidates.size() + " items."));
lastContentCandidates.addAll(newContentCandidates);
// replace the candidates list
lastContent.remove("candidates");
lastContent.add("candidates", lastContentCandidates);
// don't need to save anything, lastContent is a reference not a copy
addIt = false;
}
} catch (Exception e) {
Log.e(LOG_TAG, "## createLocalStream(): createPeerConnection - onIceCandidate() Exception Msg=" + e.getMessage());
}
}
if (addIt) {
Event event = new Event(Event.EVENT_TYPE_CALL_CANDIDATES, content, mSession.getCredentials().userId, mCallSignalingRoom.getRoomId());
mPendingEvents.add(event);
sendNextEvent();
}
}
}
});
}
@Override
public void onAddStream(final MediaStream mediaStream) {
Log.d(LOG_TAG, "## mPeerConnection creation: onAddStream " + mediaStream);
mUIThreadHandler.post(new Runnable() {
@Override
public void run() {
if ((mediaStream.videoTracks.size() == 1) && !isCallEnded()) {
mRemoteVideoTrack = mediaStream.videoTracks.get(0);
mRemoteVideoTrack.setEnabled(true);
mRemoteVideoTrack.addRenderer(mLargeRemoteRenderer);
}
}
});
}
@Override
public void onRemoveStream(final MediaStream mediaStream) {
Log.d(LOG_TAG, "## mPeerConnection creation: onRemoveStream " + mediaStream);
mUIThreadHandler.post(new Runnable() {
@Override
public void run() {
if (null != mRemoteVideoTrack) {
mRemoteVideoTrack.dispose();
mRemoteVideoTrack = null;
mediaStream.videoTracks.get(0).dispose();
}
}
});
}
@Override
public void onDataChannel(DataChannel dataChannel) {
Log.d(LOG_TAG, "## mPeerConnection creation: onDataChannel " + dataChannel);
}
@Override
public void onRenegotiationNeeded() {
Log.d(LOG_TAG, "## mPeerConnection creation: onRenegotiationNeeded");
}
});
// send our local video and audio stream to make it seen by the other part
mPeerConnection.addStream(mLocalMediaStream);
MediaConstraints constraints = new MediaConstraints();
constraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"));
constraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveVideo", isVideo() ? "true" : "false"));
// call createOffer only for outgoing calls
if (!isIncoming()) {
Log.d(LOG_TAG, "## createLocalStream(): !isIncoming() -> createOffer");
mPeerConnection.createOffer(new SdpObserver() {
@Override
public void onCreateSuccess(SessionDescription sessionDescription) {
Log.d(LOG_TAG, "createOffer onCreateSuccess");
final SessionDescription sdp = new SessionDescription(sessionDescription.type, sessionDescription.description);
mUIThreadHandler.post(new Runnable() {
@Override
public void run() {
if (mPeerConnection != null) {
// must be done to before sending the invitation message
mPeerConnection.setLocalDescription(new SdpObserver() {
@Override
public void onCreateSuccess(SessionDescription sessionDescription) {
Log.d(LOG_TAG, "setLocalDescription onCreateSuccess");
}
@Override
public void onSetSuccess() {
Log.d(LOG_TAG, "setLocalDescription onSetSuccess");
sendInvite(sdp);
dispatchOnStateDidChange(IMXCall.CALL_STATE_INVITE_SENT);
}
@Override
public void onCreateFailure(String s) {
Log.e(LOG_TAG, "setLocalDescription onCreateFailure " + s);
dispatchOnCallError(CALL_ERROR_CAMERA_INIT_FAILED);
hangup(null);
}
@Override
public void onSetFailure(String s) {
Log.e(LOG_TAG, "setLocalDescription onSetFailure " + s);
dispatchOnCallError(CALL_ERROR_CAMERA_INIT_FAILED);
hangup(null);
}
}, sdp);
}
}
});
}
@Override
public void onSetSuccess() {
Log.d(LOG_TAG, "createOffer onSetSuccess");
}
@Override
public void onCreateFailure(String s) {
Log.d(LOG_TAG, "createOffer onCreateFailure " + s);
dispatchOnCallError(CALL_ERROR_CAMERA_INIT_FAILED);
}
@Override
public void onSetFailure(String s) {
Log.d(LOG_TAG, "createOffer onSetFailure " + s);
dispatchOnCallError(CALL_ERROR_CAMERA_INIT_FAILED);
}
}, constraints);
dispatchOnStateDidChange(IMXCall.CALL_STATE_WAIT_CREATE_OFFER);
}
}
/**
* @return true if the device has a camera device
*/
private boolean hasCameraDevice() {
int devicesNumber = 0;
try {
devicesNumber = VideoCapturerAndroid.getDeviceCount();
mFrontCameraName = VideoCapturerAndroid.getNameOfFrontFacingDevice();
mBackCameraName = VideoCapturerAndroid.getNameOfBackFacingDevice();
} catch (Exception e) {
Log.e(LOG_TAG, "hasCameraDevice " + e.getLocalizedMessage());
}
Log.d(LOG_TAG, "hasCameraDevice(): camera number= " + devicesNumber);
Log.d(LOG_TAG, "hasCameraDevice(): frontCameraName=" + mFrontCameraName + " backCameraName=" + mBackCameraName);
return (null != mFrontCameraName) || (null != mBackCameraName);
}
/**
* Create the local video stack
*
* @return the video track
*/
private VideoTrack createVideoTrack() { // permission crash
Log.d(LOG_TAG, "createVideoTrack");
// create the local renderer only if there is a camera on the device
if (hasCameraDevice()) {
try {
if (null != mFrontCameraName) {
mVideoCapturer = VideoCapturerAndroid.create(mFrontCameraName);
if (null == mVideoCapturer) {
Log.e(LOG_TAG, "Cannot create Video Capturer from front camera");
} else {
mCameraInUse = CAMERA_TYPE_FRONT;
}
}
if ((null == mVideoCapturer) && (null != mBackCameraName)) {
mVideoCapturer = VideoCapturerAndroid.create(mBackCameraName);
if (null == mVideoCapturer) {
Log.e(LOG_TAG, "Cannot create Video Capturer from back camera");
} else {
mCameraInUse = CAMERA_TYPE_REAR;
}
}
} catch (Exception ex2) {
// catch exception due to Android M permissions, when
// a call is received and the permissions (camera and audio) were not yet granted
Log.e(LOG_TAG, "createVideoTrack(): Exception Msg=" + ex2.getMessage());
}
if (null != mVideoCapturer) {
Log.d(LOG_TAG, "createVideoTrack find a video capturer");
try {
MediaConstraints videoConstraints = new MediaConstraints();
videoConstraints.mandatory.add(new MediaConstraints.KeyValuePair(
MIN_VIDEO_WIDTH_CONSTRAINT, Integer.toString(MIN_VIDEO_WIDTH)));
mVideoSource = mPeerConnectionFactory.createVideoSource(mVideoCapturer, videoConstraints);
mLocalVideoTrack = mPeerConnectionFactory.createVideoTrack(VIDEO_TRACK_ID, mVideoSource);
mLocalVideoTrack.setEnabled(true);
mLocalVideoTrack.addRenderer(mLargeLocalRenderer);
} catch (Exception e) {
Log.e(LOG_TAG, "createVideoSource fails with exception " + e.getLocalizedMessage());
mLocalVideoTrack = null;
if (null != mVideoSource) {
mVideoSource.dispose();
mVideoSource = null;
}
}
} else {
Log.e(LOG_TAG, "## createVideoTrack(): Cannot create Video Capturer - no camera available");
}
}
return mLocalVideoTrack;
}
/**
* Create the local video stack
*
* @return the video track
*/
private AudioTrack createAudioTrack() {
Log.d(LOG_TAG, "createAudioTrack");
MediaConstraints audioConstraints = new MediaConstraints();
// add all existing audio filters to avoid having echos
audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googEchoCancellation", "true"));
audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googEchoCancellation2", "true"));
audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googDAEchoCancellation", "true"));
audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googTypingNoiseDetection", "true"));
audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googAutoGainControl", "true"));
audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googAutoGainControl2", "true"));
audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googNoiseSuppression", "true"));
audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googNoiseSuppression2", "true"));
audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googAudioMirroring", "false"));
audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googHighpassFilter", "true"));
mAudioSource = mPeerConnectionFactory.createAudioSource(audioConstraints);
mLocalAudioTrack = mPeerConnectionFactory.createAudioTrack(AUDIO_TRACK_ID, mAudioSource);
return mLocalAudioTrack;
}
/**
* Initialize the call UI
*
* @param callInviteParams the invite params
* @param aLocalVideoPosition position of the local video attendee
*/
private void initCallUI(final JsonObject callInviteParams, VideoLayoutConfiguration aLocalVideoPosition) {
Log.d(LOG_TAG, "## initCallUI(): IN");
if (isCallEnded()) {
Log.w(LOG_TAG, "## initCallUI(): skipped due to call is ended");
return;
}
if (isVideo()) {
Log.d(LOG_TAG, "## initCallUI(): building UI video call");
try {
// pass a runnable to be run once the surface view is ready
VideoRendererGui.setView(mCallView, new Runnable() {
@Override
public void run() {
mUIThreadHandler.post(new Runnable() {
@Override
public void run() {
if (null == mPeerConnectionFactory) {
Log.d(LOG_TAG, "## initCallUI(): video call and no mPeerConnectionFactory");
mPeerConnectionFactory = new PeerConnectionFactory();
createVideoTrack();
createAudioTrack();
createLocalStream();
if (null != callInviteParams) {
dispatchOnStateDidChange(CALL_STATE_RINGING);
setRemoteDescription(callInviteParams);
}
}
}
});
}
});
} catch (Exception e) {
// GA issue
// it seems that setView triggers some exception like "setRenderer has already been called"
Log.e(LOG_TAG, "## initCallUI(): VideoRendererGui.setView : Exception Msg =" + e.getMessage());
}
// create the renderers after the VideoRendererGui.setView
try {
Log.d(LOG_TAG, "## initCallUI() building UI");
// create the video displaying the remote view sent by the server
if (isConference()) {
mLargeRemoteRenderer = VideoRendererGui.createGui(0, 0, 100, 100, VideoRendererGui.ScalingType.SCALE_ASPECT_FIT, false);
} else {
mLargeRemoteRenderer = VideoRendererGui.createGui(0, 0, 100, 100, VideoRendererGui.ScalingType.SCALE_ASPECT_FILL, false);
}
mLargeLocalRendererCallbacks = VideoRendererGui.create(0, 0, 100, 100, VideoRendererGui.ScalingType.SCALE_ASPECT_FILL, true);
mLargeLocalRenderer = new VideoRenderer(mLargeLocalRendererCallbacks);
// create the video displaying the local user: horizontal center, just above the video buttons menu
if (null != aLocalVideoPosition) {
mSmallLocalRendererCallbacks = VideoRendererGui.create(aLocalVideoPosition.mX, aLocalVideoPosition.mY, aLocalVideoPosition.mWidth, aLocalVideoPosition.mHeight, VideoRendererGui.ScalingType.SCALE_ASPECT_BALANCED, true);
Log.d(LOG_TAG, "## initCallUI(): " + aLocalVideoPosition);
} else {
// default layout
mSmallLocalRendererCallbacks = VideoRendererGui.create(5, 5, 25, 25, VideoRendererGui.ScalingType.SCALE_ASPECT_BALANCED, true);
}
mSmallLocalRenderer = new VideoRenderer(mSmallLocalRendererCallbacks);
} catch (Exception e) {
Log.e(LOG_TAG, "## initCallUI(): Exception Msg =" + e.getMessage());
}
mCallView.setVisibility(View.VISIBLE);
} else {
Log.d(LOG_TAG, "## initCallUI(): build audio call");
// audio call
mUIThreadHandler.post(new Runnable() {
@Override
public void run() {
if (null == mPeerConnectionFactory) {
mPeerConnectionFactory = new PeerConnectionFactory();
createAudioTrack();
createLocalStream();
if (null != callInviteParams) {
dispatchOnStateDidChange(CALL_STATE_RINGING);
setRemoteDescription(callInviteParams);
}
}
}
});
}
}
// actions (must be done after dispatchOnViewReady()
/**
* The activity is paused.
*/
@Override
public void onPause() {
super.onPause();
Log.d(LOG_TAG, "onPause");
try {
if (!isCallEnded()) {
Log.d(LOG_TAG, "onPause with active call");
if (null != mCallView) {
mCallView.onPause();
}
if (mVideoSource != null && !mIsVideoSourceStopped) {
mVideoSource.stop();
mIsVideoSourceStopped = true;
}
}
} catch (Exception e) {
// race condition
Log.e(LOG_TAG, "onPause failed " + e.getLocalizedMessage());
}
}
/**
* The activity is resumed.
*/
@Override
public void onResume() {
super.onResume();
Log.d(LOG_TAG, "onResume");
try {
if (!isCallEnded()) {
Log.d(LOG_TAG, "onResume with active call");
if (null != mCallView) {
mCallView.onResume();
}
if (mVideoSource != null && mIsVideoSourceStopped) {
mVideoSource.restart();
mIsVideoSourceStopped = false;
}
mUIThreadHandler.postDelayed(new Runnable() {
@Override
public void run() {
listenPreviewUpdate();
}
}, 500);
}
} catch (Exception e) {
Log.e(LOG_TAG, "onResume failed " + e.getLocalizedMessage());
}
}
/**
* Start a call.
*/
@Override
public void placeCall(VideoLayoutConfiguration aLocalVideoPosition) {
Log.d(LOG_TAG, "placeCall");
dispatchOnStateDidChange(IMXCall.CALL_STATE_WAIT_LOCAL_MEDIA);
initCallUI(null, aLocalVideoPosition);
}
/**
* Set the remote description
*
* @param callInviteParams the invitation params
*/
private void setRemoteDescription(final JsonObject callInviteParams) {
Log.d(LOG_TAG, "setRemoteDescription " + callInviteParams);
SessionDescription aDescription = null;
// extract the description
try {
if (callInviteParams.has("offer")) {
JsonObject answer = callInviteParams.getAsJsonObject("offer");
String type = answer.get("type").getAsString();
String sdp = answer.get("sdp").getAsString();
if (!TextUtils.isEmpty(type) && !TextUtils.isEmpty(sdp)) {
aDescription = new SessionDescription(SessionDescription.Type.OFFER, sdp);
}
}
} catch (Exception e) {
Log.e(LOG_TAG, "## setRemoteDescription(): Exception Msg=" + e.getMessage());
}
mPeerConnection.setRemoteDescription(new SdpObserver() {
@Override
public void onCreateSuccess(SessionDescription sessionDescription) {
Log.d(LOG_TAG, "setRemoteDescription onCreateSuccess");
}
@Override
public void onSetSuccess() {
Log.d(LOG_TAG, "setRemoteDescription onSetSuccess");
mIsIncomingPrepared = true;
mUIThreadHandler.post(new Runnable() {
@Override
public void run() {
checkPendingCandidates();
}
});
}
@Override
public void onCreateFailure(String s) {
Log.e(LOG_TAG, "setRemoteDescription onCreateFailure " + s);
dispatchOnCallError(CALL_ERROR_CAMERA_INIT_FAILED);
}
@Override
public void onSetFailure(String s) {
Log.e(LOG_TAG, "setRemoteDescription onSetFailure " + s);
dispatchOnCallError(CALL_ERROR_CAMERA_INIT_FAILED);
}
}, aDescription);
}
/**
* Prepare a call reception.
*
* @param aCallInviteParams the invitation Event content
* @param aCallId the call ID
*/
@Override
public void prepareIncomingCall(final JsonObject aCallInviteParams, final String aCallId, final VideoLayoutConfiguration aLocalVideoPosition) {
Log.d(LOG_TAG, "## prepareIncomingCall : call state " + getCallState());
mCallId = aCallId;
if (CALL_STATE_FLEDGLING.equals(getCallState())) {
mIsIncoming = true;
dispatchOnStateDidChange(CALL_STATE_WAIT_LOCAL_MEDIA);
mUIThreadHandler.post(new Runnable() {
@Override
public void run() {
initCallUI(aCallInviteParams, aLocalVideoPosition);
}
});
} else if (CALL_STATE_CREATED.equals(getCallState())) {
mCallInviteParams = aCallInviteParams;
// detect call type from the sdp
try {
JsonObject offer = mCallInviteParams.get("offer").getAsJsonObject();
JsonElement sdp = offer.get("sdp");
String sdpValue = sdp.getAsString();
setIsVideo(sdpValue.contains("m=video"));
} catch (Exception e) {
Log.e(LOG_TAG, "## prepareIncomingCall(): Exception Msg=" + e.getMessage());
}
}
}
/**
* The call has been detected as an incoming one.
* The application launches the dedicated activity and expects to launch the incoming call.
* The local video attendee is displayed in the screen according to the values given in aLocalVideoPosition.
*
* @param aLocalVideoPosition local video position
*/
@Override
public void launchIncomingCall(VideoLayoutConfiguration aLocalVideoPosition) {
Log.d(LOG_TAG, "launchIncomingCall : call state " + getCallState());
if (CALL_STATE_FLEDGLING.equals(getCallState())) {
prepareIncomingCall(mCallInviteParams, mCallId, aLocalVideoPosition);
}
}
/**
* The callee accepts the call.
*
* @param event the event
*/
private void onCallAnswer(final Event event) {
Log.d(LOG_TAG, "onCallAnswer : call state " + getCallState());
if (!CALL_STATE_CREATED.equals(getCallState()) && (null != mPeerConnection)) {
mUIThreadHandler.post(new Runnable() {
@Override
public void run() {
dispatchOnStateDidChange(IMXCall.CALL_STATE_CONNECTING);
SessionDescription aDescription = null;
// extract the description
try {
JsonObject eventContent = event.getContentAsJsonObject();
if (eventContent.has("answer")) {
JsonObject answer = eventContent.getAsJsonObject("answer");
String type = answer.get("type").getAsString();
String sdp = answer.get("sdp").getAsString();
if (!TextUtils.isEmpty(type) && !TextUtils.isEmpty(sdp) && type.equals("answer")) {
aDescription = new SessionDescription(SessionDescription.Type.ANSWER, sdp);
}
}
} catch (Exception e) {
Log.d(LOG_TAG, "onCallAnswer : " + e.getLocalizedMessage());
}
mPeerConnection.setRemoteDescription(new SdpObserver() {
@Override
public void onCreateSuccess(SessionDescription sessionDescription) {
Log.d(LOG_TAG, "setRemoteDescription onCreateSuccess");
}
@Override
public void onSetSuccess() {
Log.d(LOG_TAG, "setRemoteDescription onSetSuccess");
}
@Override
public void onCreateFailure(String s) {
Log.e(LOG_TAG, "setRemoteDescription onCreateFailure " + s);
dispatchOnCallError(CALL_ERROR_CAMERA_INIT_FAILED);
}
@Override
public void onSetFailure(String s) {
Log.e(LOG_TAG, "setRemoteDescription onSetFailure " + s);
dispatchOnCallError(CALL_ERROR_CAMERA_INIT_FAILED);
}
}, aDescription);
}
});
}
}
/**
* The other call member hangs up the call.
*
* @param event the event
* @param hangUpReasonId hang up reason
*/
private void onCallHangup(final Event event, final int hangUpReasonId) {
Log.d(LOG_TAG, "## onCallHangup(): call state=" + getCallState());
String state = getCallState();
if (!CALL_STATE_CREATED.equals(state) && (null != mPeerConnection)) {
mUIThreadHandler.post(new Runnable() {
@Override
public void run() {
terminate(hangUpReasonId);
}
});
} else if (CALL_STATE_WAIT_LOCAL_MEDIA.equals(state) && isVideo()) {
// specific case fixing: a video call hung up by the calling side
// when the callee is still displaying the InComingCallActivity dialog.
// If terminate() was not called, the dialog was never dismissed.
mUIThreadHandler.post(new Runnable() {
@Override
public void run() {
terminate(hangUpReasonId);
}
});
}
}
/**
* A new Ice candidate is received
*
* @param candidates the channel candidates
*/
private void onNewCandidates(final JsonArray candidates) {
Log.d(LOG_TAG, "## onNewCandidates(): call state " + getCallState() + " with candidates " + candidates);
if (!CALL_STATE_CREATED.equals(getCallState()) && (null != mPeerConnection)) {
ArrayList<IceCandidate> candidatesList = new ArrayList<>();
// convert the JSON to IceCandidate
for (int index = 0; index < candidates.size(); index++) {
JsonObject item = candidates.get(index).getAsJsonObject();
try {
String candidate = item.get("candidate").getAsString();
String sdpMid = item.get("sdpMid").getAsString();
int sdpLineIndex = item.get("sdpMLineIndex").getAsInt();
candidatesList.add(new IceCandidate(sdpMid, sdpLineIndex, candidate));
} catch (Exception e) {
Log.e(LOG_TAG, "## onNewCandidates(): Exception Msg=" + e.getMessage());
}
}
for (IceCandidate cand : candidatesList) {
Log.d(LOG_TAG, "## onNewCandidates(): addIceCandidate " + cand);
mPeerConnection.addIceCandidate(cand);
}
}
}
/**
* Add ice candidates
*
* @param candidates ic candidates
*/
private void addCandidates(JsonArray candidates) {
if (mIsIncomingPrepared || !isIncoming()) {
Log.d(LOG_TAG, "addCandidates : ready");
onNewCandidates(candidates);
} else {
synchronized (LOG_TAG) {
Log.d(LOG_TAG, "addCandidates : pending");
mPendingCandidates.addAll(candidates);
}
}
}
/**
* Some Ice candidates could have been received while creating the call view.
* Check if some of them have been defined.
*/
private void checkPendingCandidates() {
Log.d(LOG_TAG, "checkPendingCandidates");
synchronized (LOG_TAG) {
onNewCandidates(mPendingCandidates);
mPendingCandidates = new JsonArray();
}
}
// events thread
/**
* Manage the call events.
*
* @param event the call event.
*/
@Override
public void handleCallEvent(Event event) {
if (event.isCallEvent()) {
String eventType = event.getType();
Log.d(LOG_TAG, "handleCallEvent " + eventType);
// event from other member
if (!TextUtils.equals(event.getSender(), mSession.getMyUserId())) {
if (Event.EVENT_TYPE_CALL_ANSWER.equals(eventType) && !mIsIncoming) {
onCallAnswer(event);
} else if (Event.EVENT_TYPE_CALL_CANDIDATES.equals(eventType)) {
JsonObject eventContent = event.getContentAsJsonObject();
JsonArray candidates = eventContent.getAsJsonArray("candidates");
addCandidates(candidates);
} else if (Event.EVENT_TYPE_CALL_HANGUP.equals(eventType)) {
onCallHangup(event, IMXCall.END_CALL_REASON_PEER_HANG_UP);
}
} else { // event from the current member, but sent from another device
switch (eventType) {
case Event.EVENT_TYPE_CALL_INVITE:
// warn in the UI thread
mUIThreadHandler.post(new Runnable() {
@Override
public void run() {
dispatchOnStateDidChange(CALL_STATE_RINGING);
}
});
break;
case Event.EVENT_TYPE_CALL_ANSWER:
// call answered from another device
mUIThreadHandler.post(new Runnable() {
@Override
public void run() {
onAnsweredElsewhere();
}
});
break;
case Event.EVENT_TYPE_CALL_HANGUP:
// current member answered elsewhere
onCallHangup(event, IMXCall.END_CALL_REASON_PEER_HANG_UP_ELSEWHERE);
break;
default:
break;
} // switch end
}
}
}
// user actions
/**
* The call is accepted.
*/
@Override
public void answer() {
Log.d(LOG_TAG, "answer " + getCallState());
if (!CALL_STATE_CREATED.equals(getCallState()) && (null != mPeerConnection)) {
mUIThreadHandler.post(new Runnable() {
@Override
public void run() {
if (null == mPeerConnection) {
Log.d(LOG_TAG, "answer the connection has been closed");
return;
}
dispatchOnStateDidChange(CALL_STATE_CREATE_ANSWER);
MediaConstraints constraints = new MediaConstraints();
constraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"));
constraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveVideo", isVideo() ? "true" : "false"));
mPeerConnection.createAnswer(new SdpObserver() {
@Override
public void onCreateSuccess(SessionDescription sessionDescription) {
Log.d(LOG_TAG, "createAnswer onCreateSuccess");
final SessionDescription sdp = new SessionDescription(sessionDescription.type, sessionDescription.description);
mUIThreadHandler.post(new Runnable() {
@Override
public void run() {
if (mPeerConnection != null) {
// must be done to before sending the invitation message
mPeerConnection.setLocalDescription(new SdpObserver() {
@Override
public void onCreateSuccess(SessionDescription sessionDescription) {
Log.d(LOG_TAG, "setLocalDescription onCreateSuccess");
}
@Override
public void onSetSuccess() {
Log.d(LOG_TAG, "setLocalDescription onSetSuccess");
sendAnswer(sdp);
dispatchOnStateDidChange(IMXCall.CALL_STATE_CONNECTING);
}
@Override
public void onCreateFailure(String s) {
Log.e(LOG_TAG, "setLocalDescription onCreateFailure " + s);
dispatchOnCallError(CALL_ERROR_CAMERA_INIT_FAILED);
hangup(null);
}
@Override
public void onSetFailure(String s) {
Log.e(LOG_TAG, "setLocalDescription onSetFailure " + s);
dispatchOnCallError(CALL_ERROR_CAMERA_INIT_FAILED);
hangup(null);
}
}, sdp);
}
}
});
}
@Override
public void onSetSuccess() {
Log.d(LOG_TAG, "createAnswer onSetSuccess");
}
@Override
public void onCreateFailure(String s) {
Log.e(LOG_TAG, "createAnswer onCreateFailure " + s);
dispatchOnCallError(CALL_ERROR_CAMERA_INIT_FAILED);
hangup(null);
}
@Override
public void onSetFailure(String s) {
Log.e(LOG_TAG, "createAnswer onSetFailure " + s);
dispatchOnCallError(CALL_ERROR_CAMERA_INIT_FAILED);
hangup(null);
}
}, constraints);
}
});
}
}
/**
* The call is hung up.
*/
@Override
public void hangup(String reason) {
Log.d(LOG_TAG, "## hangup(): reason=" + reason);
if (!isCallEnded()) {
sendHangup(reason);
terminate(IMXCall.END_CALL_REASON_UNDEFINED);
}
}
/**
* @return the callstate (must be a CALL_STATE_XX value)
*/
@Override
public String getCallState() {
return mCallState;
}
/**
* @return the callView
*/
@Override
public View getCallView() {
return mCallView;
}
/**
* @return the callView visibility
*/
@Override
public int getVisibility() {
if (null != mCallView) {
return mCallView.getVisibility();
} else {
return View.GONE;
}
}
/**
* Set the callview visibility
*
* @return true if the operation succeeds
*/
@Override
public boolean setVisibility(int visibility) {
if (null != mCallView) {
mCallView.setVisibility(visibility);
return true;
}
return false;
}
/**
* The call has been answered on another device.
* We distinguish the case where an account is active on
* multiple devices and a video call is launched on the account. In this case
* the callee who did not answer must display a "answered elsewhere" message.
*/
@Override
public void onAnsweredElsewhere() {
String state = getCallState();
Log.d(LOG_TAG, "onAnsweredElsewhere in state " + state);
if (!isCallEnded() && !mIsAnswered) {
dispatchAnsweredElsewhere();
terminate(IMXCall.END_CALL_REASON_UNDEFINED);
}
}
@Override
protected void dispatchOnStateDidChange(String newState) {
Log.d(LOG_TAG, "dispatchOnStateDidChange " + newState);
mCallState = newState;
// call timeout management
if (CALL_STATE_CONNECTING.equals(mCallState) || CALL_STATE_CONNECTING.equals(mCallState)) {
if (null != mCallTimeoutTimer) {
mCallTimeoutTimer.cancel();
mCallTimeoutTimer = null;
}
}
super.dispatchOnStateDidChange(newState);
}
//==============================================================================================================
// Preview size management
//==============================================================================================================
/**
* @return the device rotation angle
*/
private int getDeviceOrientation() {
try {
WindowManager wm = (WindowManager) this.mContext.getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
short orientation1;
switch (wm.getDefaultDisplay().getRotation()) {
case Surface.ROTATION_0:
default:
orientation1 = 0;
break;
case Surface.ROTATION_90:
orientation1 = 90;
break;
case Surface.ROTATION_180:
orientation1 = 180;
break;
case Surface.ROTATION_270:
orientation1 = 270;
}
return orientation1;
} catch (Exception e) {
Log.e(LOG_TAG, "## getDeviceOrientation() failed " + e.getMessage());
}
return 0;
}
/**
* The camera preview frame has been updated
*
* @param camera the camera
* @param cameraOrientation the camera orientation
*/
private void onPreviewFrameUpdate(Camera camera, int cameraOrientation) {
Camera.Size s;
try {
s = camera.getParameters().getPreviewSize();
} catch (Exception e) {
Log.e(LOG_TAG, "## onPreviewFrameUpdate() failed " + e.getMessage());
return;
}
final int width;
final int height;
int rotation = (360 + cameraOrientation + getDeviceOrientation()) % 360;
if ((rotation == 90) || (rotation == 270)) {
width = s.height;
height = s.width;
} else {
width = s.width;
height = s.height;
}
if ((width != mLocalRenderWidth) || (height != mLocalRenderHeight)) {
mLocalRenderWidth = width;
mLocalRenderHeight = height;
mUIThreadHandler.post(new Runnable() {
@Override
public void run() {
dispatchOnPreviewSizeChanged(width, height);
}
});
}
}
/**
* Define a listener to track the local frame update.
*/
private void listenPreviewUpdate() {
try {
if (null != mVideoCapturer) {
Field field = mVideoCapturer.getClass().getDeclaredField("camera");
field.setAccessible(true);
Camera camera = (Camera) field.get(mVideoCapturer);
if (null != camera) {
try {
Camera.CameraInfo info = new android.hardware.Camera.CameraInfo();
Camera.getCameraInfo(mCameraInUse == CAMERA_TYPE_FRONT ? android.hardware.Camera.CameraInfo.CAMERA_FACING_FRONT : android.hardware.Camera.CameraInfo.CAMERA_FACING_BACK, info);
final int cameraOrientation = info.orientation;
camera.setPreviewCallbackWithBuffer(new Camera.PreviewCallback() {
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
onPreviewFrameUpdate(camera, cameraOrientation);
try {
((VideoCapturerAndroid) mVideoCapturer).onPreviewFrame(data, camera);
} catch (Exception e) {
Log.e(LOG_TAG, "## listenPreviewUpdate() : onPreviewFrame failed " + e.getMessage());
}
}
});
onPreviewFrameUpdate(camera, cameraOrientation);
} catch (Exception e) {
Log.e(LOG_TAG, "## listenPreviewUpdate() : fail to update the camera preview " + e.getMessage());
}
} else {
Log.e(LOG_TAG, "## listenPreviewUpdate() : did not find the camera");
}
}
} catch (Exception e) {
Log.e(LOG_TAG, "## listenPreviewUpdate() failed " + e.getMessage());
}
}
}