Merge branch 'video-streaming-android' into 'development'

Merge video call into 'development'

See merge request Cloud_Solution/doctor_app_flutter!745
merge-requests/746/head
Mohammad Aljammal 5 years ago
commit 78338ebc64

@ -70,6 +70,7 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'

@ -27,7 +27,6 @@
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:label="HMG Doctor">
<activity android:name=".ui.VideoCallActivity"></activity>
<activity
android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"

@ -2,101 +2,177 @@ package com.hmg.hmgDr
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.annotation.NonNull
import com.google.gson.GsonBuilder
import com.hmg.hmgDr.Model.GetSessionStatusModel
import com.hmg.hmgDr.Model.SessionStatusModel
import com.hmg.hmgDr.ui.VideoCallActivity
import com.google.gson.GsonBuilder
import com.hmg.hmgDr.ui.VideoCallResponseListener
import com.hmg.hmgDr.ui.fragment.VideoCallFragment
import io.flutter.embedding.android.FlutterFragmentActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugins.GeneratedPluginRegistrant
class MainActivity : FlutterFragmentActivity(), MethodChannel.MethodCallHandler {
class MainActivity : FlutterFragmentActivity(), MethodChannel.MethodCallHandler, VideoCallResponseListener {
private val CHANNEL = "Dr.cloudSolution/videoCall"
private lateinit var methodChannel: MethodChannel
private var result: MethodChannel.Result? = null
private var call: MethodCall? = null
private val LAUNCH_VIDEO: Int = 1
private var dialogFragment: VideoCallFragment? = null
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
GeneratedPluginRegistrant.registerWith(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler(this)
methodChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
methodChannel.setMethodCallHandler(this)
}
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
this.result = result
this.call = call
if (call.method == "openVideoCall") {
val apiKey = call.argument<String>("kApiKey")
val sessionId = call.argument<String>("kSessionId")
val token = call.argument<String>("kToken")
val appLang = call.argument<String>("appLang")
val baseUrl = call.argument<String>("baseUrl")
when (call.method) {
"openVideoCall" -> {
val apiKey = call.argument<String>("kApiKey")
val sessionId = call.argument<String>("kSessionId")
val token = call.argument<String>("kToken")
val appLang = call.argument<String>("appLang")
val baseUrl = call.argument<String>("baseUrl")
// Session Status model
val VC_ID = call.argument<Int>("VC_ID")
val tokenID = call.argument<String>("TokenID")
val generalId = call.argument<String>("generalId")
val doctorId = call.argument<Int>("DoctorId")
val patientName = call.argument<String>("patientName")
// Session Status model
val VC_ID = call.argument<Int>("VC_ID")
val tokenID = call.argument<String>("TokenID")
val generalId = call.argument<String>("generalId")
val doctorId = call.argument<Int>("DoctorId")
val patientName = call.argument<String>("patientName")
val sessionStatusModel = GetSessionStatusModel(VC_ID, tokenID, generalId, doctorId, patientName)
val sessionStatusModel = GetSessionStatusModel(VC_ID, tokenID, generalId, doctorId, patientName)
openVideoCall(apiKey, sessionId, token, appLang, baseUrl, sessionStatusModel)
openVideoCall(apiKey, sessionId, token, appLang, baseUrl, sessionStatusModel)
}
"closeVideoCall" -> {
dialogFragment?.onCallClicked()
}
"onCallConnected" -> {
} else {
result.notImplemented()
}
else -> {
result.notImplemented()
}
}
}
private fun openVideoCall(apiKey: String?, sessionId: String?, token: String?, appLang: String?, baseUrl: String?, sessionStatusModel: GetSessionStatusModel) {
// val videoCallActivity = VideoCallActivity()
if (dialogFragment == null) {
val arguments = Bundle()
arguments.putString("apiKey", apiKey)
arguments.putString("sessionId", sessionId)
arguments.putString("token", token)
arguments.putString("appLang", appLang)
arguments.putString("baseUrl", baseUrl)
arguments.putParcelable("sessionStatusModel", sessionStatusModel)
val transaction = supportFragmentManager.beginTransaction()
dialogFragment = VideoCallFragment.newInstance(arguments)
dialogFragment?.let {
it.setCallListener(this)
it.isCancelable = true
if (it.isAdded){
it.dismiss()
}else {
it.show(transaction, "dialog")
}
val intent = Intent(this, VideoCallActivity::class.java)
intent.putExtra("apiKey", apiKey)
intent.putExtra("sessionId", sessionId)
intent.putExtra("token", token)
intent.putExtra("appLang", appLang)
intent.putExtra("baseUrl", baseUrl)
intent.putExtra("sessionStatusModel", sessionStatusModel)
startActivityForResult(intent, LAUNCH_VIDEO)
}
} else if (!dialogFragment!!.isVisible) {
val transaction = supportFragmentManager.beginTransaction()
dialogFragment!!.show(transaction, "dialog")
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
var asd = "";
if (requestCode == LAUNCH_VIDEO) {
if (resultCode == Activity.RESULT_OK) {
val result : SessionStatusModel? = data?.getParcelableExtra("sessionStatusNotRespond")
val callResponse : HashMap<String, String> = HashMap()
/* override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
var asd = "";
if (requestCode == LAUNCH_VIDEO) {
if (resultCode == Activity.RESULT_OK) {
val result : SessionStatusModel? = data?.getParcelableExtra("sessionStatusNotRespond")
val callResponse : HashMap<String, String> = HashMap()
val sessionStatus : HashMap<String, String> = HashMap()
val gson = GsonBuilder().serializeNulls().create()
val sessionStatus : HashMap<String, String> = HashMap()
val gson = GsonBuilder().serializeNulls().create()
callResponse["callResponse"] = "CallNotRespond"
val jsonRes = gson.toJson(result)
callResponse["sessionStatus"] = jsonRes
callResponse["callResponse"] = "CallNotRespond"
val jsonRes = gson.toJson(result)
callResponse["sessionStatus"] = jsonRes
this.result?.success(callResponse)
}
if (resultCode == Activity.RESULT_CANCELED) {
val callResponse : HashMap<String, String> = HashMap()
callResponse["callResponse"] = "CallEnd"
result?.success(callResponse)
}
}
}*/
override fun onCallFinished(resultCode: Int, intent: Intent?) {
dialogFragment = null
if (resultCode == Activity.RESULT_OK) {
val result: SessionStatusModel? = intent?.getParcelableExtra("sessionStatusNotRespond")
val callResponse: HashMap<String, String> = HashMap()
val sessionStatus: HashMap<String, String> = HashMap()
val gson = GsonBuilder().serializeNulls().create()
callResponse["callResponse"] = "CallNotRespond"
val jsonRes = gson.toJson(result)
callResponse["sessionStatus"] = jsonRes
try {
this.result?.success(callResponse)
} catch (e : Exception){
Log.e("onVideoCallFinished", "${e.message}.")
}
if (resultCode == Activity.RESULT_CANCELED) {
val callResponse : HashMap<String, String> = HashMap()
callResponse["callResponse"] = "CallEnd"
} else if (resultCode == Activity.RESULT_CANCELED) {
val callResponse: HashMap<String, String> = HashMap()
callResponse["callResponse"] = "CallEnd"
try {
result?.success(callResponse)
} catch (e : Exception){
Log.e("onVideoCallFinished", "${e.message}.")
}
}
}
override fun errorHandle(message: String) {
dialogFragment = null
// Toast.makeText(this, message, Toast.LENGTH_LONG).show()
}
override fun minimizeVideoEvent(isMinimize: Boolean) {
if (isMinimize)
methodChannel.invokeMethod("onCallConnected", null)
else
methodChannel.invokeMethod("onCallDisconnected", null)
}
override fun onBackPressed() {
super.onBackPressed()
}
}

@ -1,482 +0,0 @@
package com.hmg.hmgDr.ui;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Intent;
import android.opengl.GLSurfaceView;
import android.os.Bundle;
import android.os.CountDownTimer;
import android.os.Handler;
import android.os.SystemClock;
import android.util.Log;
import android.view.View;
import android.widget.Chronometer;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import com.hmg.hmgDr.Model.ChangeCallStatusRequestModel;
import com.hmg.hmgDr.Model.GetSessionStatusModel;
import com.hmg.hmgDr.Model.SessionStatusModel;
import com.hmg.hmgDr.R;
import com.opentok.android.Session;
import com.opentok.android.Stream;
import com.opentok.android.Publisher;
import com.opentok.android.PublisherKit;
import com.opentok.android.Subscriber;
import com.opentok.android.BaseVideoRenderer;
import com.opentok.android.OpentokError;
import com.opentok.android.SubscriberKit;
import java.util.List;
import java.util.Objects;
import pub.devrel.easypermissions.AfterPermissionGranted;
import pub.devrel.easypermissions.AppSettingsDialog;
import pub.devrel.easypermissions.EasyPermissions;
public class VideoCallActivity extends AppCompatActivity implements EasyPermissions.PermissionCallbacks,
Session.SessionListener,
Publisher.PublisherListener,
Subscriber.VideoListener, VideoCallContract.VideoCallView {
private static final String TAG = VideoCallActivity.class.getSimpleName();
VideoCallContract.VideoCallPresenter videoCallPresenter;
private static final int RC_SETTINGS_SCREEN_PERM = 123;
private static final int RC_VIDEO_APP_PERM = 124;
private Session mSession;
private Publisher mPublisher;
private Subscriber mSubscriber;
private Handler mVolHandler, mConnectedHandler;
private Runnable mVolRunnable, mConnectedRunnable;
private FrameLayout mPublisherViewContainer;
private RelativeLayout mSubscriberViewContainer;
private RelativeLayout controlPanel;
private String apiKey;
private String sessionId;
private String token;
private String appLang;
private String baseUrl;
private boolean isSwitchCameraClicked;
private boolean isCameraClicked;
private boolean isSpeckerClicked;
private boolean isMicClicked;
private TextView patientName;
private Chronometer cmTimer;
long elapsedTime;
Boolean resume = false;
private ImageView mCallBtn;
private ImageView btnMinimize;
private ImageView mCameraBtn;
private ImageView mSwitchCameraBtn;
private ImageView mspeckerBtn;
private ImageView mMicBtn;
private ProgressBar progressBar;
private CountDownTimer countDownTimer;
private TextView progressBarTextView;
private RelativeLayout progressBarLayout;
private boolean isConnected = false;
private GetSessionStatusModel sessionStatusModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
setTheme(R.style.AppTheme);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_video_call);
Objects.requireNonNull(getSupportActionBar()).hide();
initUI();
requestPermissions();
}
@Override
protected void onPause() {
super.onPause();
if (mSession == null) {
return;
}
mSession.onPause();
if (isFinishing()) {
disconnectSession();
}
}
@Override
protected void onResume() {
super.onResume();
if (mSession == null) {
return;
}
mSession.onResume();
}
@Override
protected void onDestroy() {
disconnectSession();
cmTimer.stop();
super.onDestroy();
}
@SuppressLint("ClickableViewAccessibility")
private void initUI() {
mPublisherViewContainer = findViewById(R.id.local_video_view_container);
mSubscriberViewContainer = findViewById(R.id.remote_video_view_container);
apiKey = getIntent().getStringExtra("apiKey");
sessionId = getIntent().getStringExtra("sessionId");
token = getIntent().getStringExtra("token");
appLang = getIntent().getStringExtra("appLang");
baseUrl = getIntent().getStringExtra("baseUrl");
sessionStatusModel = getIntent().getParcelableExtra("sessionStatusModel");
controlPanel = findViewById(R.id.control_panel);
videoCallPresenter = new VideoCallPresenterImpl(this, baseUrl);
patientName = findViewById(R.id.patient_name);
patientName.setText(sessionStatusModel.getPatientName());
cmTimer = findViewById(R.id.cmTimer);
cmTimer.setFormat("mm:ss");
cmTimer.setOnChronometerTickListener(arg0 -> {
long minutes;
long seconds;
if (!resume) {
minutes = ((SystemClock.elapsedRealtime() - cmTimer.getBase()) / 1000) / 60;
seconds = ((SystemClock.elapsedRealtime() - cmTimer.getBase()) / 1000) % 60;
elapsedTime = SystemClock.elapsedRealtime();
} else {
minutes = ((elapsedTime - cmTimer.getBase()) / 1000) / 60;
seconds = ((elapsedTime - cmTimer.getBase()) / 1000) % 60;
elapsedTime = elapsedTime + 1000;
}
Log.d(TAG, "onChronometerTick: " + minutes + " : " + seconds);
});
mCallBtn = findViewById(R.id.btn_call);
btnMinimize = findViewById(R.id.btn_minimize);
mCameraBtn = findViewById(R.id.btn_camera);
mSwitchCameraBtn = findViewById(R.id.btn_switch_camera);
mspeckerBtn = findViewById(R.id.btn_specker);
mMicBtn = findViewById(R.id.btn_mic);
// progressBarLayout=findViewById(R.id.progressBar);
// progressBar=findViewById(R.id.progress_bar);
// progressBarTextView=findViewById(R.id.progress_bar_text);
// progressBar.setVisibility(View.GONE);
hiddenButtons();
checkClientConnected();
mSubscriberViewContainer.setOnTouchListener((v, event) -> {
controlPanel.setVisibility(View.VISIBLE);
mVolHandler.removeCallbacks(mVolRunnable);
mVolHandler.postDelayed(mVolRunnable, 5 * 1000);
return true;
});
if (appLang.equals("ar")) {
progressBarLayout.setLayoutDirection(View.LAYOUT_DIRECTION_RTL);
}
}
private void checkClientConnected() {
mConnectedHandler = new Handler();
mConnectedRunnable = () -> {
if (!isConnected) {
videoCallPresenter.callClintConnected(sessionStatusModel);
}
};
mConnectedHandler.postDelayed(mConnectedRunnable, 55 * 1000);
}
private void hiddenButtons() {
mVolHandler = new Handler();
mVolRunnable = () -> controlPanel.setVisibility(View.GONE);
mVolHandler.postDelayed(mVolRunnable, 5 * 1000);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this);
}
@Override
public void onPermissionsGranted(int requestCode, List<String> perms) {
Log.d(TAG, "onPermissionsGranted:" + requestCode + ":" + perms.size());
}
@Override
public void onPermissionsDenied(int requestCode, List<String> perms) {
Log.d(TAG, "onPermissionsDenied:" + requestCode + ":" + perms.size());
if (EasyPermissions.somePermissionPermanentlyDenied(this, perms)) {
new AppSettingsDialog.Builder(this)
.setTitle(getString(R.string.title_settings_dialog))
.setRationale(getString(R.string.rationale_ask_again))
.setPositiveButton(getString(R.string.setting))
.setNegativeButton(getString(R.string.cancel))
.setRequestCode(RC_SETTINGS_SCREEN_PERM)
.build()
.show();
}
}
@AfterPermissionGranted(RC_VIDEO_APP_PERM)
private void requestPermissions() {
String[] perms = {Manifest.permission.INTERNET, Manifest.permission.CAMERA,};
if (EasyPermissions.hasPermissions(this, perms)) {
try {
mSession = new Session.Builder(this, apiKey, sessionId).build();
mSession.setSessionListener(this);
mSession.connect(token);
} catch (Exception e) {
e.printStackTrace();
}
} else {
EasyPermissions.requestPermissions(this, getString(R.string.remaining_ar), RC_VIDEO_APP_PERM, perms);
}
}
@Override
public void onConnected(Session session) {
Log.i(TAG, "Session Connected");
mPublisher = new Publisher.Builder(this).build();
mPublisher.setPublisherListener(this);
mPublisherViewContainer.addView(mPublisher.getView());
if (mPublisher.getView() instanceof GLSurfaceView) {
((GLSurfaceView) mPublisher.getView()).setZOrderOnTop(true);
}
mSession.publish(mPublisher);
if (!resume) {
cmTimer.setBase(SystemClock.elapsedRealtime());
}
cmTimer.start();
}
@Override
public void onDisconnected(Session session) {
Log.d(TAG, "onDisconnected: disconnected from session " + session.getSessionId());
mSession = null;
cmTimer.stop();
}
@Override
public void onError(Session session, OpentokError opentokError) {
Log.d(TAG, "onError: Error (" + opentokError.getMessage() + ") in session " + session.getSessionId());
// Toast.makeText(this, "Session error. See the logcat please.", Toast.LENGTH_LONG).show();
//finish();
}
@Override
public void onStreamReceived(Session session, Stream stream) {
Log.d(TAG, "onStreamReceived: New stream " + stream.getStreamId() + " in session " + session.getSessionId());
if (mSubscriber != null) {
isConnected = true;
return;
}
isConnected = true;
subscribeToStream(stream);
if(mConnectedHandler!=null && mConnectedRunnable!=null)
mConnectedHandler.removeCallbacks(mConnectedRunnable);
videoCallPresenter.callChangeCallStatus(new ChangeCallStatusRequestModel(3,sessionStatusModel.getDoctorId(), sessionStatusModel.getGeneralid(),token,sessionStatusModel.getVCID()));
}
@Override
public void onStreamDropped(Session session, Stream stream) {
Log.d(TAG, "onStreamDropped: Stream " + stream.getStreamId() + " dropped from session " + session.getSessionId());
if (mSubscriber == null) {
return;
}
if (mSubscriber.getStream().equals(stream)) {
mSubscriberViewContainer.removeView(mSubscriber.getView());
mSubscriber.destroy();
mSubscriber = null;
}
disconnectSession();
}
@Override
public void onStreamCreated(PublisherKit publisherKit, Stream stream) {
Log.d(TAG, "onStreamCreated: Own stream " + stream.getStreamId() + " created");
}
@Override
public void onStreamDestroyed(PublisherKit publisherKit, Stream stream) {
Log.d(TAG, "onStreamDestroyed: Own stream " + stream.getStreamId() + " destroyed");
}
@Override
public void onError(PublisherKit publisherKit, OpentokError opentokError) {
Log.d(TAG, "onError: Error (" + opentokError.getMessage() + ") in publisher");
// Toast.makeText(this, "onError: Error (" + opentokError.getMessage() + ") in publisher", Toast.LENGTH_LONG).show();
// finish();
}
@Override
public void onVideoDataReceived(SubscriberKit subscriberKit) {
mSubscriber.setStyle(BaseVideoRenderer.STYLE_VIDEO_SCALE, BaseVideoRenderer.STYLE_VIDEO_FILL);
mSubscriberViewContainer.addView(mSubscriber.getView());
}
@Override
public void onVideoDisabled(SubscriberKit subscriberKit, String s) {
}
@Override
public void onVideoEnabled(SubscriberKit subscriberKit, String s) {
}
@Override
public void onVideoDisableWarning(SubscriberKit subscriberKit) {
}
@Override
public void onVideoDisableWarningLifted(SubscriberKit subscriberKit) {
}
private void subscribeToStream(Stream stream) {
mSubscriber = new Subscriber.Builder(VideoCallActivity.this, stream).build();
mSubscriber.setVideoListener(this);
mSession.subscribe(mSubscriber);
}
private void disconnectSession() {
if (mSession == null) {
setResult(Activity.RESULT_CANCELED);
finish();
return;
}
if (mSubscriber != null) {
mSubscriberViewContainer.removeView(mSubscriber.getView());
mSession.unsubscribe(mSubscriber);
mSubscriber.destroy();
mSubscriber = null;
}
if (mPublisher != null) {
mPublisherViewContainer.removeView(mPublisher.getView());
mSession.unpublish(mPublisher);
mPublisher.destroy();
mPublisher = null;
}
mSession.disconnect();
if (countDownTimer != null) {
countDownTimer.cancel();
}
videoCallPresenter.callChangeCallStatus(new ChangeCallStatusRequestModel(16,sessionStatusModel.getDoctorId(), sessionStatusModel.getGeneralid(),token,sessionStatusModel.getVCID()));
finish();
}
public void onMinimizedClicked(View view) {
}
public void onSwitchCameraClicked(View view) {
if (mPublisher != null) {
isSwitchCameraClicked = !isSwitchCameraClicked;
mPublisher.cycleCamera();
int res = isSwitchCameraClicked ? R.drawable.flip_disapled : R.drawable.flip_enabled;
mSwitchCameraBtn.setImageResource(res);
}
}
public void onCameraClicked(View view) {
if (mPublisher != null) {
isCameraClicked = !isCameraClicked;
mPublisher.setPublishVideo(!isCameraClicked);
int res = isCameraClicked ? R.drawable.video_disanabled : R.drawable.video_enabled;
mCameraBtn.setImageResource(res);
}
}
public void onSpeckerClicked(View view) {
if (mSubscriber != null) {
isSpeckerClicked = !isSpeckerClicked;
mSubscriber.setSubscribeToAudio(!isSpeckerClicked);
int res = isSpeckerClicked ? R.drawable.audio_disabled : R.drawable.audio_enabled;
mspeckerBtn.setImageResource(res);
}
}
public void onMicClicked(View view) {
if (mPublisher != null) {
isMicClicked = !isMicClicked;
mPublisher.setPublishAudio(!isMicClicked);
int res = isMicClicked ? R.drawable.mic_disabled : R.drawable.mic_enabled;
mMicBtn.setImageResource(res);
}
}
public void onCallClicked(View view) {
disconnectSession();
}
@Override
public void onCallSuccessful(SessionStatusModel sessionStatusModel) {
if (sessionStatusModel.getSessionStatus() == 2 || sessionStatusModel.getSessionStatus() == 3) {
Intent returnIntent = new Intent();
returnIntent.putExtra("sessionStatusNotRespond", sessionStatusModel);
setResult(Activity.RESULT_OK, returnIntent);
finish();
} else if( sessionStatusModel.getSessionStatus() == 4 ){
isConnected = true;
if(mConnectedHandler!=null && mConnectedRunnable!=null)
mConnectedHandler.removeCallbacks(mConnectedRunnable);
}
}
@Override
public void onCallChangeCallStatusSuccessful(SessionStatusModel sessionStatusModel) {
}
@Override
public void onFailure() {
}
}

@ -0,0 +1,14 @@
package com.hmg.hmgDr.ui
import android.content.Intent
interface VideoCallResponseListener {
fun onCallFinished(resultCode : Int, intent: Intent? = null)
fun errorHandle(message: String)
fun minimizeVideoEvent(isMinimize : Boolean)
fun onBackHandle(){}
}

@ -0,0 +1,893 @@
package com.hmg.hmgDr.ui.fragment
import android.Manifest
import android.annotation.SuppressLint
import android.app.Activity
import android.app.Dialog
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.graphics.Point
import android.graphics.drawable.ColorDrawable
import android.opengl.GLSurfaceView
import android.os.*
import android.util.Log
import android.view.*
import android.widget.*
import androidx.annotation.Nullable
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
import androidx.core.view.GestureDetectorCompat
import androidx.fragment.app.DialogFragment
import com.hmg.hmgDr.Model.ChangeCallStatusRequestModel
import com.hmg.hmgDr.Model.GetSessionStatusModel
import com.hmg.hmgDr.Model.SessionStatusModel
import com.hmg.hmgDr.R
import com.hmg.hmgDr.ui.VideoCallContract.VideoCallPresenter
import com.hmg.hmgDr.ui.VideoCallContract.VideoCallView
import com.hmg.hmgDr.ui.VideoCallPresenterImpl
import com.hmg.hmgDr.ui.VideoCallResponseListener
import com.hmg.hmgDr.util.DynamicVideoRenderer
import com.hmg.hmgDr.util.ThumbnailCircleVideoRenderer
import com.opentok.android.*
import com.opentok.android.PublisherKit.PublisherListener
import pub.devrel.easypermissions.AfterPermissionGranted
import pub.devrel.easypermissions.AppSettingsDialog
import pub.devrel.easypermissions.EasyPermissions
import pub.devrel.easypermissions.EasyPermissions.PermissionCallbacks
import kotlin.math.ceil
class VideoCallFragment : DialogFragment(), PermissionCallbacks, Session.SessionListener, PublisherListener,
SubscriberKit.VideoListener, VideoCallView {
private var isFullScreen: Boolean = true
private var isCircle: Boolean = false
private var x_init_cord = 0
private var y_init_cord: Int = 0
private var x_init_margin: Int = 0
private var y_init_margin: Int = 0
private val szWindow: Point = Point()
private lateinit var mWindowManager: WindowManager
private var isLeft = true
private lateinit var videoCallPresenter: VideoCallPresenter
private var mSession: Session? = null
private var mPublisher: Publisher? = null
private var mSubscriber: Subscriber? = null
private var mVolHandler: Handler? = null
private var mConnectedHandler: Handler? = null
private var mVolRunnable: Runnable? = null
private var mConnectedRunnable: Runnable? = null
private lateinit var thumbnail_container: FrameLayout
private lateinit var mPublisherViewContainer: FrameLayout
private lateinit var mPublisherViewIcon: View
private lateinit var mSubscriberViewContainer: FrameLayout
private lateinit var mSubscriberViewIcon: ImageView
private lateinit var controlPanel: ConstraintLayout
private var apiKey: String? = null
private var sessionId: String? = null
private var token: String? = null
private var appLang: String? = null
private var baseUrl: String? = null
private var isSwitchCameraClicked = false
private var isCameraClicked = false
private var isSpeckerClicked = false
private var isMicClicked = false
private lateinit var parentView: View
private lateinit var videoCallContainer: ConstraintLayout
private lateinit var layoutName: RelativeLayout
private lateinit var layoutMini: RelativeLayout
private lateinit var icMini: ImageButton
private lateinit var mCallBtn: ImageView
private lateinit var btnMinimize: ImageView
private lateinit var mCameraBtn: ImageView
private lateinit var mSwitchCameraBtn: ImageView
private lateinit var mspeckerBtn: ImageView
private lateinit var mMicBtn: ImageView
private lateinit var patientName: TextView
private lateinit var cmTimer: Chronometer
private var elapsedTime: Long = 0
private var resume = false
private val progressBar: ProgressBar? = null
private val countDownTimer: CountDownTimer? = null
private val progressBarTextView: TextView? = null
private val progressBarLayout: RelativeLayout? = null
private var isConnected = false
private var sessionStatusModel: GetSessionStatusModel? = null
private var videoCallResponseListener: VideoCallResponseListener? = null
private lateinit var mDetector: GestureDetectorCompat
override fun onCreate(savedInstanceState: Bundle?) {
requireActivity().setTheme(R.style.AppTheme)
super.onCreate(savedInstanceState)
}
override fun onStart() {
super.onStart()
dialog?.window?.setLayout(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT
)
}
override fun getTheme(): Int {
return R.style.dialogTheme
}
override fun onCreateDialog(@Nullable savedInstanceState: Bundle?): Dialog {
val dialog: Dialog = super.onCreateDialog(savedInstanceState)
// Add back button listener
// Add back button listener
dialog.setOnKeyListener { _, keyCode, keyEvent ->
// getAction to make sure this doesn't double fire
if (keyCode == KeyEvent.KEYCODE_BACK && keyEvent.action == KeyEvent.ACTION_UP) {
videoCallResponseListener?.onBackHandle()
false // Capture onKey
} else true
// Don't capture
}
return dialog
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// This is done in a post() since the dialog must be drawn before locating.
requireView().post {
val dialogWindow = dialog!!.window
if (dialog != null && dialogWindow != null) {
dialogWindow.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT));
}
// Make the dialog possible to be outside touch
dialogWindow!!.setFlags(
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
)
dialogWindow.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
requireView().invalidate()
}
}
fun setCallListener(videoCallResponseListener: VideoCallResponseListener) {
this.videoCallResponseListener = videoCallResponseListener
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View {
parentView = inflater.inflate(R.layout.activity_video_call, container, false)
// Objects.requireNonNull(requireActivity().actionBar)!!.hide()
arguments?.run {
apiKey = getString("apiKey")
sessionId = getString("sessionId")
token = getString("token")
appLang = getString("appLang")
baseUrl = getString("baseUrl")
sessionStatusModel = getParcelable("sessionStatusModel")
}
initUI(parentView)
requestPermissions()
handleDragDialog()
mDetector = GestureDetectorCompat(context, MyGestureListener({ showControlPanelTemporarily() }, { miniCircleDoubleTap() }))
return parentView
}
override fun onPause() {
super.onPause()
if (mSession == null) {
return
}
mSession!!.onPause()
if (requireActivity().isFinishing) {
disconnectSession()
}
}
override fun onResume() {
super.onResume()
if (mSession == null) {
return
}
mSession!!.onResume()
}
override fun onDestroy() {
disconnectSession()
cmTimer.stop()
super.onDestroy()
}
@SuppressLint("ClickableViewAccessibility")
private fun initUI(view: View) {
videoCallContainer = view.findViewById(R.id.video_call_ll)
layoutName = view.findViewById(R.id.layout_name)
layoutMini = view.findViewById(R.id.layout_mini)
icMini = view.findViewById(R.id.ic_mini)
thumbnail_container = view.findViewById(R.id.thumbnail_container)
mPublisherViewContainer = view.findViewById(R.id.local_video_view_container)
mPublisherViewIcon = view.findViewById(R.id.local_video_view_icon)
mSubscriberViewIcon = view.findViewById(R.id.remote_video_view_icon)
mSubscriberViewContainer = view.findViewById(R.id.remote_video_view_container)
patientName = view.findViewById<TextView>(R.id.patient_name)
patientName.text = sessionStatusModel!!.patientName
cmTimer = view.findViewById<Chronometer>(R.id.cmTimer)
cmTimer.format = "mm:ss"
cmTimer.onChronometerTickListener = Chronometer.OnChronometerTickListener { arg0: Chronometer? ->
val minutes: Long
val seconds: Long
if (!resume) {
minutes = (SystemClock.elapsedRealtime() - cmTimer.base) / 1000 / 60
seconds = (SystemClock.elapsedRealtime() - cmTimer.base) / 1000 % 60
elapsedTime = SystemClock.elapsedRealtime()
} else {
minutes = (elapsedTime - cmTimer.base) / 1000 / 60
seconds = (elapsedTime - cmTimer.base) / 1000 % 60
elapsedTime += 1000
}
arg0?.text = "$minutes:$seconds"
Log.d(VideoCallFragment.TAG, "onChronometerTick: $minutes : $seconds")
}
icMini.setOnClickListener {
onMiniCircleClicked()
}
controlPanel = view.findViewById(R.id.control_panel)
videoCallPresenter = VideoCallPresenterImpl(this, baseUrl)
mCallBtn = view.findViewById<ImageView>(R.id.btn_call)
mCallBtn.setOnClickListener {
onCallClicked()
}
btnMinimize = view.findViewById<ImageView>(R.id.btn_minimize)
btnMinimize.setOnClickListener {
onMinimizedClicked(it)
}
mCameraBtn = view.findViewById<ImageView>(R.id.btn_camera)
mCameraBtn.setOnClickListener {
onCameraClicked(it)
}
mSwitchCameraBtn = view.findViewById<ImageView>(R.id.btn_switch_camera)
mSwitchCameraBtn.setOnClickListener {
onSwitchCameraClicked(it)
}
mspeckerBtn = view.findViewById<ImageView>(R.id.btn_specker)
mspeckerBtn.setOnClickListener {
onSpeckerClicked(it)
}
mMicBtn = view.findViewById<ImageView>(R.id.btn_mic)
mMicBtn.setOnClickListener {
onMicClicked(it)
}
// progressBarLayout=findViewById(R.id.progressBar);
// progressBar=findViewById(R.id.progress_bar);
// progressBarTextView=findViewById(R.id.progress_bar_text);
// progressBar.setVisibility(View.GONE);
hiddenButtons()
checkClientConnected()
if (appLang == "ar") {
progressBarLayout!!.layoutDirection = View.LAYOUT_DIRECTION_RTL
}
}
private fun checkClientConnected() {
mConnectedHandler = Handler((Looper.getMainLooper()))
mConnectedRunnable = Runnable {
if (!isConnected) {
videoCallPresenter.callClintConnected(sessionStatusModel)
}
}
mConnectedHandler!!.postDelayed(mConnectedRunnable!!, (55 * 1000).toLong())
}
private fun hiddenButtons() {
mVolHandler = Handler()
mVolRunnable = Runnable { controlPanel.visibility = View.GONE }
mVolHandler!!.postDelayed(mVolRunnable!!, (5 * 1000).toLong())
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String?>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this)
}
override fun onPermissionsGranted(requestCode: Int, perms: List<String?>) {
Log.d(TAG, "onPermissionsGranted:" + requestCode + ":" + perms.size)
}
override fun onPermissionsDenied(requestCode: Int, perms: List<String?>) {
Log.d(TAG, "onPermissionsDenied:" + requestCode + ":" + perms.size)
if (EasyPermissions.somePermissionPermanentlyDenied(this, perms)) {
AppSettingsDialog.Builder(this)
.setTitle(getString(R.string.title_settings_dialog))
.setRationale(getString(R.string.rationale_ask_again))
.setPositiveButton(getString(R.string.setting))
.setNegativeButton(getString(R.string.cancel))
.setRequestCode(RC_SETTINGS_SCREEN_PERM)
.build()
.show()
}
}
@AfterPermissionGranted(RC_VIDEO_APP_PERM)
private fun requestPermissions() {
val perms = arrayOf(Manifest.permission.INTERNET, Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO, Manifest.permission.MODIFY_AUDIO_SETTINGS, Manifest.permission.CALL_PHONE)
if (EasyPermissions.hasPermissions(requireContext(), *perms)) {
try {
mSession = Session.Builder(context, apiKey, sessionId).build()
mSession!!.setSessionListener(this)
mSession!!.connect(token)
} catch (e: Exception) {
e.printStackTrace()
}
} else {
EasyPermissions.requestPermissions(this, getString(R.string.remaining_ar), RC_VIDEO_APP_PERM, *perms)
}
}
override fun onConnected(session: Session?) {
Log.i(TAG, "Session Connected")
mPublisher = Publisher.Builder(requireContext())
// .name("publisher")
// .renderer(ThumbnailCircleVideoRenderer(requireContext()))
.build()
mPublisher!!.setPublisherListener(this)
if (mPublisher!!.view is GLSurfaceView) {
(mPublisher!!.view as GLSurfaceView).setZOrderOnTop(true)
}
mPublisherViewContainer.addView(mPublisher!!.view)
mSession!!.publish(mPublisher)
if (!resume) {
cmTimer.base = SystemClock.elapsedRealtime()
}
cmTimer.start()
videoCallResponseListener?.minimizeVideoEvent(true)
}
override fun onDisconnected(session: Session) {
Log.d(TAG, "onDisconnected: disconnected from session " + session.sessionId)
mSession = null
cmTimer.stop()
disconnectSession()
videoCallResponseListener?.minimizeVideoEvent(false)
}
override fun onError(session: Session, opentokError: OpentokError) {
Log.d(TAG, "onError: Error (" + opentokError.message + ") in session " + session.sessionId)
// videoCallResponseListener?.errorHandle("Error (" + opentokError.message + ") in session ")
// dialog?.dismiss()
}
override fun onStreamReceived(session: Session, stream: Stream) {
Log.d(TAG, "onStreamReceived: New stream " + stream.streamId + " in session " + session.sessionId)
if (mSubscriber != null) {
isConnected = true
return
}
isConnected = true
subscribeToStream(stream)
if (mConnectedHandler != null && mConnectedRunnable != null)
mConnectedHandler!!.removeCallbacks(mConnectedRunnable!!)
videoCallPresenter.callChangeCallStatus(ChangeCallStatusRequestModel(3, sessionStatusModel!!.doctorId, sessionStatusModel!!.generalid, token, sessionStatusModel!!.vcid))
}
override fun onStreamDropped(session: Session, stream: Stream) {
Log.d(TAG, "onStreamDropped: Stream " + stream.streamId + " dropped from session " + session.sessionId)
if (mSubscriber == null) {
return
}
if (mSubscriber!!.stream == stream) {
mSubscriberViewContainer.removeView(mSubscriber!!.view)
mSubscriber!!.destroy()
mSubscriber = null
}
disconnectSession()
}
override fun onStreamCreated(publisherKit: PublisherKit?, stream: Stream) {
Log.d(TAG, "onStreamCreated: Own stream " + stream.streamId + " created")
}
override fun onStreamDestroyed(publisherKit: PublisherKit?, stream: Stream) {
Log.d(TAG, "onStreamDestroyed: Own stream " + stream.streamId + " destroyed")
}
override fun onError(publisherKit: PublisherKit?, opentokError: OpentokError) {
Log.d(VideoCallFragment.TAG, "onError: Error (" + opentokError.message + ") in publisher")
// videoCallResponseListener?.errorHandle("Error (" + opentokError.message + ") in publisher")
// dialog?.dismiss()
}
override fun onVideoDataReceived(subscriberKit: SubscriberKit?) {
mSubscriber!!.setStyle(BaseVideoRenderer.STYLE_VIDEO_SCALE, BaseVideoRenderer.STYLE_VIDEO_FILL)
(mSubscriber!!.renderer as DynamicVideoRenderer).enableThumbnailCircle(false)
mSubscriberViewContainer.addView(mSubscriber!!.view)
// switchToThumbnailCircle()
}
fun switchToThumbnailCircle() {
thumbnail_container.postDelayed({
val view = mSubscriber!!.view
if (view.parent != null) {
(view.parent as ViewGroup).removeView(view)
}
if (view is GLSurfaceView) {
view.setZOrderOnTop(true)
if (mSubscriber!!.renderer is DynamicVideoRenderer) {
(mSubscriber!!.renderer as DynamicVideoRenderer).enableThumbnailCircle(true)
thumbnail_container.addView(view)
}
}
switchToFullScreenView()
}, 4000)
}
fun switchToFullScreenView() {
mSubscriberViewContainer.postDelayed({
val view = mSubscriber!!.view
if (view.parent != null) {
(view.parent as ViewGroup).removeView(view)
}
if (view is GLSurfaceView) {
view.setZOrderOnTop(false)
if (mSubscriber!!.renderer is DynamicVideoRenderer) {
(mSubscriber!!.renderer as DynamicVideoRenderer).enableThumbnailCircle(false)
mSubscriberViewContainer.addView(view)
}
}
switchToThumbnailCircle()
}, 4000)
}
override fun onVideoDisabled(subscriberKit: SubscriberKit?, s: String?) {}
override fun onVideoEnabled(subscriberKit: SubscriberKit?, s: String?) {}
override fun onVideoDisableWarning(subscriberKit: SubscriberKit?) {}
override fun onVideoDisableWarningLifted(subscriberKit: SubscriberKit?) {}
private fun subscribeToStream(stream: Stream) {
mSubscriber = Subscriber.Builder(requireContext(), stream)
.renderer(DynamicVideoRenderer(requireContext()))
.build()
mSubscriber!!.setVideoListener(this)
mSession!!.subscribe(mSubscriber)
}
private fun disconnectSession() {
if (mSession == null) {
videoCallResponseListener?.onCallFinished(Activity.RESULT_CANCELED)
// requireActivity().setResult(Activity.RESULT_CANCELED)
dialog?.dismiss()
return
}
if (mSubscriber != null) {
mSubscriberViewContainer.removeView(mSubscriber!!.view)
mSession!!.unsubscribe(mSubscriber)
mSubscriber!!.destroy()
mSubscriber = null
}
if (mPublisher != null) {
mPublisherViewContainer.removeView(mPublisher!!.view)
mSession!!.unpublish(mPublisher)
mPublisher!!.destroy()
mPublisher = null
}
mSession!!.disconnect()
countDownTimer?.cancel()
videoCallPresenter.callChangeCallStatus(ChangeCallStatusRequestModel(16, sessionStatusModel!!.doctorId, sessionStatusModel!!.generalid, token, sessionStatusModel!!.vcid))
dialog?.dismiss()
}
override fun onCallSuccessful(sessionStatusModel: SessionStatusModel) {
if (sessionStatusModel.sessionStatus == 2 || sessionStatusModel.sessionStatus == 3) {
val returnIntent = Intent()
returnIntent.putExtra("sessionStatusNotRespond", sessionStatusModel)
videoCallResponseListener?.onCallFinished(Activity.RESULT_OK, returnIntent)
// requireActivity().setResult(Activity.RESULT_OK, returnIntent)
dialog?.dismiss()
}
}
override fun onCallChangeCallStatusSuccessful(sessionStatusModel: SessionStatusModel?) {}
override fun onFailure() {}
private fun onSwitchCameraClicked(view: View?) {
if (mPublisher != null) {
isSwitchCameraClicked = !isSwitchCameraClicked
mPublisher!!.cycleCamera()
val res = if (isSwitchCameraClicked) R.drawable.camera_front else R.drawable.camera_back
mSwitchCameraBtn.setImageResource(res)
}
}
fun onCallClicked() {
disconnectSession()
}
private fun miniCircleDoubleTap() {
if (isCircle) {
onMiniCircleClicked()
}
}
private fun onMiniCircleClicked() {
if (isCircle) {
dialog?.window?.setLayout(
400,
600
)
} else {
dialog?.window?.setLayout(
300,
300
)
}
isCircle = !isCircle
if (mSubscriber != null) {
(mSubscriber!!.renderer as DynamicVideoRenderer).enableThumbnailCircle(isCircle)
} else {
if (isCircle) {
videoCallContainer.background = ContextCompat.getDrawable(requireContext(), R.drawable.circle_shape)
mSubscriberViewContainer.background = ContextCompat.getDrawable(requireContext(), R.drawable.circle_shape)
} else {
videoCallContainer.setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.text_color))
mSubscriberViewContainer.setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.text_color))
}
}
if (isCircle) {
controlPanel.visibility = View.GONE
layoutMini.visibility = View.GONE
} else {
controlPanel.visibility = View.VISIBLE
layoutMini.visibility = View.VISIBLE
}
}
private fun onMinimizedClicked(view: View?) {
if (isFullScreen) {
dialog?.window?.setLayout(
400,
600
)
} else {
dialog?.window?.setLayout(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT
)
}
isFullScreen = !isFullScreen
val res = if (isFullScreen) R.drawable.reducing else R.drawable.expand
btnMinimize.setImageResource(res)
setViewsVisibility()
// videoCallResponseListener?.minimizeVideoEvent(!isFullScreen)
}
private fun setViewsVisibility() {
val iconSize: Int = context!!.resources.getDimension(R.dimen.video_icon_size).toInt()
val iconSizeSmall: Int = context!!.resources.getDimension(R.dimen.video_icon_size_small).toInt()
val btnMinimizeLayoutParam: ConstraintLayout.LayoutParams = btnMinimize.layoutParams as ConstraintLayout.LayoutParams
val mCallBtnLayoutParam: ConstraintLayout.LayoutParams = mCallBtn.layoutParams as ConstraintLayout.LayoutParams
val localPreviewMargin: Int = context!!.resources.getDimension(R.dimen.local_preview_margin_top).toInt()
val localPreviewWidth: Int = context!!.resources.getDimension(R.dimen.local_preview_width).toInt()
val localPreviewHeight: Int = context!!.resources.getDimension(R.dimen.local_preview_height).toInt()
// val localPreviewIconSize: Int = context!!.resources.getDimension(R.dimen.local_back_icon_size).toInt()
// val localPreviewMarginSmall : Int = context!!.resources.getDimension(R.dimen.local_preview_margin_small).toInt()
// val localPreviewWidthSmall : Int = context!!.resources.getDimension(R.dimen.local_preview_width_small).toInt()
// val localPreviewHeightSmall : Int = context!!.resources.getDimension(R.dimen.local_preview_height_small).toInt()
// val localPreviewIconSmall: Int = context!!.resources.getDimension(R.dimen.local_back_icon_size_small).toInt()
// val localPreviewLayoutIconParam : FrameLayout.LayoutParams
val localPreviewLayoutParam: RelativeLayout.LayoutParams = mPublisherViewContainer.layoutParams as RelativeLayout.LayoutParams
val remotePreviewIconSize: Int = context!!.resources.getDimension(R.dimen.remote_back_icon_size).toInt()
val remotePreviewIconSizeSmall: Int = context!!.resources.getDimension(R.dimen.remote_back_icon_size_small).toInt()
val remotePreviewLayoutParam: FrameLayout.LayoutParams = mSubscriberViewIcon.layoutParams as FrameLayout.LayoutParams
if (isFullScreen) {
layoutName.visibility = View.VISIBLE
layoutMini.visibility = View.GONE
mCameraBtn.visibility = View.VISIBLE
mSwitchCameraBtn.visibility = View.VISIBLE
// mspeckerBtn.visibility = View.VISIBLE
mMicBtn.visibility = View.VISIBLE
btnMinimizeLayoutParam.width = iconSize
btnMinimizeLayoutParam.height = iconSize
mCallBtnLayoutParam.width = iconSize
mCallBtnLayoutParam.height = iconSize
// localPreviewLayoutIconParam = FrameLayout.LayoutParams(localPreviewIconSize, localPreviewIconSize)
//// localPreviewLayoutParam = RelativeLayout.LayoutParams(localPreviewWidth, localPreviewHeight)
localPreviewLayoutParam.width = localPreviewWidth
localPreviewLayoutParam.height = localPreviewHeight
localPreviewLayoutParam.setMargins(0, localPreviewMargin, localPreviewMargin, 0)
// remotePreviewLayoutParam = FrameLayout.LayoutParams(remotePreviewIconSize, remotePreviewIconSize)
remotePreviewLayoutParam.width = remotePreviewIconSize
remotePreviewLayoutParam.height = remotePreviewIconSize
} else {
layoutName.visibility = View.GONE
layoutMini.visibility = View.VISIBLE
mCameraBtn.visibility = View.GONE
mSwitchCameraBtn.visibility = View.GONE
// mspeckerBtn.visibility = View.GONE
mMicBtn.visibility = View.GONE
// mPublisherViewContainer.visibility = View.GONE
// mPublisherViewIcon.visibility = View.GONE
// layoutParam = ConstraintLayout.LayoutParams(iconSizeSmall, iconSizeSmall)
btnMinimizeLayoutParam.width = iconSizeSmall
btnMinimizeLayoutParam.height = iconSizeSmall
mCallBtnLayoutParam.width = iconSizeSmall
mCallBtnLayoutParam.height = iconSizeSmall
localPreviewLayoutParam.width = 0
localPreviewLayoutParam.height = 0
localPreviewLayoutParam.setMargins(0, localPreviewMargin / 2, localPreviewMargin / 2, 0)
// localPreviewLayoutIconParam = FrameLayout.LayoutParams(localPreviewIconSmall, localPreviewIconSmall)
//// localPreviewLayoutParam = RelativeLayout.LayoutParams(localPreviewWidthSmall, localPreviewHeightSmall)
// localPreviewLayoutParam.width = localPreviewWidthSmall
// localPreviewLayoutParam.height = localPreviewWidthSmall
// localPreviewLayoutParam.setMargins(0,localPreviewMarginSmall, localPreviewMarginSmall, 0)
// remotePreviewLayoutParam = FrameLayout.LayoutParams(remotePreviewIconSizeSmall, remotePreviewIconSizeSmall)
remotePreviewLayoutParam.width = remotePreviewIconSizeSmall
remotePreviewLayoutParam.height = remotePreviewIconSizeSmall
if (isCircle) {
controlPanel.visibility = View.GONE
layoutMini.visibility = View.GONE
} else {
controlPanel.visibility = View.VISIBLE
layoutMini.visibility = View.VISIBLE
}
}
mPublisherViewContainer.layoutParams = localPreviewLayoutParam
// mPublisherViewIcon.layoutParams = localPreviewLayoutIconParam
mSubscriberViewIcon.layoutParams = remotePreviewLayoutParam
btnMinimize.layoutParams = btnMinimizeLayoutParam
mCallBtn.layoutParams = mCallBtnLayoutParam
}
private fun onCameraClicked(view: View?) {
if (mPublisher != null) {
isCameraClicked = !isCameraClicked
mPublisher!!.publishVideo = !isCameraClicked
val res = if (isCameraClicked) R.drawable.video_disabled else R.drawable.video_enabled
mCameraBtn!!.setImageResource(res)
}
}
private fun onMicClicked(view: View?) {
if (mPublisher != null) {
isMicClicked = !isMicClicked
mPublisher!!.publishAudio = !isMicClicked
val res = if (isMicClicked) R.drawable.mic_disabled else R.drawable.mic_enabled
mMicBtn!!.setImageResource(res)
}
}
private fun onSpeckerClicked(view: View?) {
if (mSubscriber != null) {
isSpeckerClicked = !isSpeckerClicked
mSubscriber!!.subscribeToAudio = !isSpeckerClicked
val res = if (isSpeckerClicked) R.drawable.audio_disabled else R.drawable.audio_enabled
mspeckerBtn.setImageResource(res)
}
}
@SuppressLint("ClickableViewAccessibility")
private fun handleDragDialog() {
mWindowManager = requireActivity().getSystemService(Context.WINDOW_SERVICE) as WindowManager
getWindowManagerDefaultDisplay()
videoCallContainer.setOnTouchListener(dragListener)
mSubscriberViewContainer.setOnTouchListener(dragListener)
}
@SuppressLint("ClickableViewAccessibility")
private val dragListener: View.OnTouchListener = View.OnTouchListener { _, event ->
mDetector.onTouchEvent(event)
//Get Floating widget view params
val layoutParams: WindowManager.LayoutParams = dialog!!.window!!.attributes
//get the touch location coordinates
val x_cord = event.rawX.toInt()
val y_cord = event.rawY.toInt()
val x_cord_Destination: Int
var y_cord_Destination: Int
when (event.action) {
MotionEvent.ACTION_DOWN -> {
x_init_cord = x_cord
y_init_cord = y_cord
//remember the initial position.
x_init_margin = layoutParams.x
y_init_margin = layoutParams.y
}
MotionEvent.ACTION_UP -> {
//Get the difference between initial coordinate and current coordinate
val x_diff: Int = x_cord - x_init_cord
val y_diff: Int = y_cord - y_init_cord
y_cord_Destination = y_init_margin + y_diff
val barHeight: Int = getStatusBarHeight()
if (y_cord_Destination < 0) {
// y_cord_Destination = 0
// y_cord_Destination =
// -(szWindow.y - (videoCallContainer.height /*+ barHeight*/))
y_cord_Destination = -(szWindow.y / 2)
} else if (y_cord_Destination + (videoCallContainer.height + barHeight) > szWindow.y) {
// y_cord_Destination = szWindow.y - (videoCallContainer.height + barHeight)
y_cord_Destination = (szWindow.y / 2)
}
layoutParams.y = y_cord_Destination
//reset position if user drags the floating view
resetPosition(x_cord)
}
MotionEvent.ACTION_MOVE -> {
val x_diff_move: Int = x_cord - x_init_cord
val y_diff_move: Int = y_cord - y_init_cord
x_cord_Destination = x_init_margin + x_diff_move
y_cord_Destination = y_init_margin + y_diff_move
layoutParams.x = x_cord_Destination
layoutParams.y = y_cord_Destination
dialog!!.window!!.attributes = layoutParams
}
}
true
}
private fun showControlPanelTemporarily() {
if (!isCircle) {
controlPanel.visibility = View.VISIBLE
mVolHandler!!.removeCallbacks(mVolRunnable!!)
mVolHandler!!.postDelayed(mVolRunnable!!, (5 * 1000).toLong())
}
}
/* Reset position of Floating Widget view on dragging */
private fun resetPosition(x_cord_now: Int) {
if (x_cord_now <= szWindow.x / 2) {
isLeft = true
moveToLeft(x_cord_now)
} else {
isLeft = false
moveToRight(x_cord_now)
}
}
/* Method to move the Floating widget view to Left */
private fun moveToLeft(current_x_cord: Int) {
var mParams: WindowManager.LayoutParams = dialog!!.window!!.attributes
mParams.x =
(szWindow.x - current_x_cord * current_x_cord - videoCallContainer.width).toInt()
dialog!!.window!!.attributes = mParams
val x = szWindow.x - current_x_cord
object : CountDownTimer(500, 5) {
//get params of Floating Widget view
var mParams: WindowManager.LayoutParams = dialog!!.window!!.attributes
override fun onTick(t: Long) {
val step = (500 - t) / 5
// mParams.x = 0 - (current_x_cord * current_x_cord * step).toInt()
mParams.x =
(szWindow.x - current_x_cord * current_x_cord * step - videoCallContainer.width).toInt()
dialog!!.window!!.attributes = mParams
}
override fun onFinish() {
mParams.x = -(szWindow.x - videoCallContainer.width)
dialog!!.window!!.attributes = mParams
}
}.start()
}
/* Method to move the Floating widget view to Right */
private fun moveToRight(current_x_cord: Int) {
// var mParams : WindowManager.LayoutParams = dialog!!.window!!.attributes
// mParams.x =
// (szWindow.x + current_x_cord * current_x_cord - videoCallContainer.width).toInt()
//
// dialog!!.window!!.attributes = mParams
object : CountDownTimer(500, 5) {
//get params of Floating Widget view
var mParams: WindowManager.LayoutParams = dialog!!.window!!.attributes
override fun onTick(t: Long) {
val step = (500 - t) / 5
mParams.x =
(szWindow.x + current_x_cord * current_x_cord * step - videoCallContainer.width).toInt()
dialog!!.window!!.attributes = mParams
}
override fun onFinish() {
mParams.x = szWindow.x - videoCallContainer.width
dialog!!.window!!.attributes = mParams
}
}.start()
}
private fun getWindowManagerDefaultDisplay() {
mWindowManager.getDefaultDisplay()
.getSize(szWindow)
}
/* return status bar height on basis of device display metrics */
private fun getStatusBarHeight(): Int {
return ceil(
(25 * requireActivity().applicationContext.resources.displayMetrics.density).toDouble()
).toInt()
}
private class MyGestureListener(val onTabCall: () -> Unit, val miniCircleDoubleTap: () -> Unit) : GestureDetector.SimpleOnGestureListener() {
override fun onSingleTapConfirmed(event: MotionEvent): Boolean {
onTabCall()
return true
}
override fun onDoubleTap(e: MotionEvent?): Boolean {
miniCircleDoubleTap()
return super.onDoubleTap(e)
}
}
companion object {
@JvmStatic
fun newInstance(args: Bundle) =
VideoCallFragment().apply {
arguments = args
}
private val TAG = VideoCallFragment::class.java.simpleName
private const val RC_SETTINGS_SCREEN_PERM = 123
private const val RC_VIDEO_APP_PERM = 124
}
}

@ -0,0 +1,379 @@
package com.hmg.hmgDr.util
import android.content.Context
import android.content.res.Resources
import android.graphics.PixelFormat
import android.opengl.GLES20
import android.opengl.GLSurfaceView
import android.opengl.Matrix
import android.view.View
import com.opentok.android.BaseVideoRenderer
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.nio.FloatBuffer
import java.nio.ShortBuffer
import java.util.concurrent.locks.ReentrantLock
import javax.microedition.khronos.egl.EGLConfig
import javax.microedition.khronos.opengles.GL10
/*
* https://nhancv.medium.com/android-how-to-make-a-circular-view-as-a-thumbnail-of-opentok-27992aee15c9
* to solve make circle video stream
* */
class DynamicVideoRenderer(private val mContext: Context) : BaseVideoRenderer() {
private val mView: GLSurfaceView = GLSurfaceView(mContext)
private val mRenderer: MyRenderer
interface DynamicVideoRendererMetadataListener {
fun onMetadataReady(metadata: ByteArray?)
}
fun setDynamicVideoRendererMetadataListener(metadataListener: DynamicVideoRendererMetadataListener?) {
mRenderer.metadataListener = metadataListener
}
fun enableThumbnailCircle(enable: Boolean) {
mRenderer.requestEnableThumbnailCircle = enable
}
internal class MyRenderer : GLSurfaceView.Renderer {
var mTextureIds = IntArray(3)
var mScaleMatrix = FloatArray(16)
private val mVertexBuffer: FloatBuffer
private val mTextureBuffer: FloatBuffer
private val mDrawListBuffer: ShortBuffer
var requestEnableThumbnailCircle = false
var mVideoFitEnabled = true
var mVideoDisabled = false
private val mVertexIndex = shortArrayOf(0, 1, 2, 0, 2, 3) // order to draw
// vertices
private val vertexShaderCode = """uniform mat4 uMVPMatrix;attribute vec4 aPosition;
attribute vec2 aTextureCoord;
varying vec2 vTextureCoord;
void main() {
gl_Position = uMVPMatrix * aPosition;
vTextureCoord = aTextureCoord;
}
"""
private val fragmentShaderCode = """precision mediump float;
uniform sampler2D Ytex;
uniform sampler2D Utex,Vtex;
uniform int enableCircle;
uniform vec2 radiusDp;
varying vec2 vTextureCoord;
void main(void) {
float nx,ny,r,g,b,y,u,v;
mediump vec4 txl,ux,vx; nx=vTextureCoord[0];
ny=vTextureCoord[1];
y=texture2D(Ytex,vec2(nx,ny)).r;
u=texture2D(Utex,vec2(nx,ny)).r;
v=texture2D(Vtex,vec2(nx,ny)).r;
y=1.1643*(y-0.0625);
u=u-0.5;
v=v-0.5;
r=y+1.5958*v;
g=y-0.39173*u-0.81290*v;
b=y+2.017*u;
if (enableCircle > 0) {
float radius = 0.5;
vec4 color0 = vec4(0.0, 0.0, 0.0, 0.0);
vec4 color1 = vec4(r, g, b, 1.0);
vec2 st = (gl_FragCoord.xy/radiusDp.xy); float dist = radius - distance(st,vec2(0.5));
float t = 1.0;
if (dist < 0.0) t = 0.0;
gl_FragColor = mix(color0, color1, t);
}
else {
gl_FragColor = vec4(r, g, b, 1.0);
}
}
"""
var mFrameLock = ReentrantLock()
var mCurrentFrame: Frame? = null
private var mProgram = 0
private var mTextureWidth = 0
private var mTextureHeight = 0
private var mViewportWidth = 0
private var mViewportHeight = 0
override fun onSurfaceCreated(gl: GL10, config: EGLConfig) {
gl.glClearColor(0f, 0f, 0f, 1f)
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
val vertexShader = loadShader(GLES20.GL_VERTEX_SHADER,
vertexShaderCode)
val fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER,
fragmentShaderCode)
mProgram = GLES20.glCreateProgram() // create empty OpenGL ES
// Program
GLES20.glAttachShader(mProgram, vertexShader) // add the vertex
// shader to program
GLES20.glAttachShader(mProgram, fragmentShader) // add the fragment
// shader to
// program
GLES20.glLinkProgram(mProgram)
val positionHandle = GLES20.glGetAttribLocation(mProgram,
"aPosition")
val textureHandle = GLES20.glGetAttribLocation(mProgram,
"aTextureCoord")
GLES20.glVertexAttribPointer(positionHandle, COORDS_PER_VERTEX,
GLES20.GL_FLOAT, false, COORDS_PER_VERTEX * 4,
mVertexBuffer)
GLES20.glEnableVertexAttribArray(positionHandle)
GLES20.glVertexAttribPointer(textureHandle,
TEXTURECOORDS_PER_VERTEX, GLES20.GL_FLOAT, false,
TEXTURECOORDS_PER_VERTEX * 4, mTextureBuffer)
GLES20.glEnableVertexAttribArray(textureHandle)
GLES20.glUseProgram(mProgram)
var i = GLES20.glGetUniformLocation(mProgram, "Ytex")
GLES20.glUniform1i(i, 0) /* Bind Ytex to texture unit 0 */
i = GLES20.glGetUniformLocation(mProgram, "Utex")
GLES20.glUniform1i(i, 1) /* Bind Utex to texture unit 1 */
i = GLES20.glGetUniformLocation(mProgram, "Vtex")
GLES20.glUniform1i(i, 2) /* Bind Vtex to texture unit 2 */
val radiusDpLocation = GLES20.glGetUniformLocation(mProgram, "radiusDp")
val radiusDp = (Resources.getSystem().displayMetrics.density * THUMBNAIL_SIZE).toInt()
GLES20.glUniform2f(radiusDpLocation, radiusDp.toFloat(), radiusDp.toFloat())
mTextureWidth = 0
mTextureHeight = 0
}
fun enableThumbnailCircle(enable: Boolean) {
GLES20.glUseProgram(mProgram)
val enableCircleLocation = GLES20.glGetUniformLocation(mProgram, "enableCircle")
GLES20.glUniform1i(enableCircleLocation, if (enable) 1 else 0)
}
fun setupTextures(frame: Frame) {
if (mTextureIds[0] != 0) {
GLES20.glDeleteTextures(3, mTextureIds, 0)
}
GLES20.glGenTextures(3, mTextureIds, 0)
val w = frame.width
val h = frame.height
val hw = w + 1 shr 1
val hh = h + 1 shr 1
initializeTexture(GLES20.GL_TEXTURE0, mTextureIds[0], w, h)
initializeTexture(GLES20.GL_TEXTURE1, mTextureIds[1], hw, hh)
initializeTexture(GLES20.GL_TEXTURE2, mTextureIds[2], hw, hh)
mTextureWidth = frame.width
mTextureHeight = frame.height
}
fun updateTextures(frame: Frame) {
val width = frame.width
val height = frame.height
val half_width = width + 1 shr 1
val half_height = height + 1 shr 1
val y_size = width * height
val uv_size = half_width * half_height
val bb = frame.buffer
// If we are reusing this frame, make sure we reset position and
// limit
bb.clear()
if (bb.remaining() == y_size + uv_size * 2) {
bb.position(0)
GLES20.glPixelStorei(GLES20.GL_UNPACK_ALIGNMENT, 1)
GLES20.glPixelStorei(GLES20.GL_PACK_ALIGNMENT, 1)
GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureIds[0])
GLES20.glTexSubImage2D(GLES20.GL_TEXTURE_2D, 0, 0, 0, width,
height, GLES20.GL_LUMINANCE, GLES20.GL_UNSIGNED_BYTE,
bb)
bb.position(y_size)
GLES20.glActiveTexture(GLES20.GL_TEXTURE1)
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureIds[1])
GLES20.glTexSubImage2D(GLES20.GL_TEXTURE_2D, 0, 0, 0,
half_width, half_height, GLES20.GL_LUMINANCE,
GLES20.GL_UNSIGNED_BYTE, bb)
bb.position(y_size + uv_size)
GLES20.glActiveTexture(GLES20.GL_TEXTURE2)
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureIds[2])
GLES20.glTexSubImage2D(GLES20.GL_TEXTURE_2D, 0, 0, 0,
half_width, half_height, GLES20.GL_LUMINANCE,
GLES20.GL_UNSIGNED_BYTE, bb)
} else {
mTextureWidth = 0
mTextureHeight = 0
}
}
override fun onSurfaceChanged(gl: GL10, width: Int, height: Int) {
GLES20.glViewport(0, 0, width, height)
mViewportWidth = width
mViewportHeight = height
}
var metadataListener: DynamicVideoRendererMetadataListener? = null
override fun onDrawFrame(gl: GL10) {
gl.glClearColor(0f, 0f, 0f, 0f)
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
mFrameLock.lock()
if (mCurrentFrame != null && !mVideoDisabled) {
GLES20.glUseProgram(mProgram)
if (mTextureWidth != mCurrentFrame!!.width
|| mTextureHeight != mCurrentFrame!!.height) {
setupTextures(mCurrentFrame!!)
}
updateTextures(mCurrentFrame!!)
Matrix.setIdentityM(mScaleMatrix, 0)
var scaleX = 1.0f
var scaleY = 1.0f
val ratio = (mCurrentFrame!!.width.toFloat()
/ mCurrentFrame!!.height)
val vratio = mViewportWidth.toFloat() / mViewportHeight
if (mVideoFitEnabled) {
if (ratio > vratio) {
scaleY = vratio / ratio
} else {
scaleX = ratio / vratio
}
} else {
if (ratio < vratio) {
scaleY = vratio / ratio
} else {
scaleX = ratio / vratio
}
}
Matrix.scaleM(mScaleMatrix, 0,
scaleX * if (mCurrentFrame!!.isMirroredX) -1.0f else 1.0f,
scaleY, 1f)
metadataListener?.onMetadataReady(mCurrentFrame!!.metadata)
val mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram,
"uMVPMatrix")
GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false,
mScaleMatrix, 0)
enableThumbnailCircle(requestEnableThumbnailCircle)
GLES20.glDrawElements(GLES20.GL_TRIANGLES, mVertexIndex.size,
GLES20.GL_UNSIGNED_SHORT, mDrawListBuffer)
} else {
//black frame when video is disabled
gl.glClearColor(0f, 0f, 0f, 1f)
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
}
mFrameLock.unlock()
}
fun displayFrame(frame: Frame?) {
mFrameLock.lock()
if (mCurrentFrame != null) {
mCurrentFrame!!.recycle()
}
mCurrentFrame = frame
mFrameLock.unlock()
}
fun disableVideo(b: Boolean) {
mFrameLock.lock()
mVideoDisabled = b
if (mVideoDisabled) {
if (mCurrentFrame != null) {
mCurrentFrame!!.recycle()
}
mCurrentFrame = null
}
mFrameLock.unlock()
}
fun enableVideoFit(enableVideoFit: Boolean) {
mVideoFitEnabled = enableVideoFit
}
companion object {
// number of coordinates per vertex in this array
const val COORDS_PER_VERTEX = 3
const val TEXTURECOORDS_PER_VERTEX = 2
var mXYZCoords = floatArrayOf(
-1.0f, 1.0f, 0.0f, // top left
-1.0f, -1.0f, 0.0f, // bottom left
1.0f, -1.0f, 0.0f, // bottom right
1.0f, 1.0f, 0.0f // top right
)
var mUVCoords = floatArrayOf(0f, 0f, 0f, 1f, 1f, 1f, 1f, 0f)
fun initializeTexture(name: Int, id: Int, width: Int, height: Int) {
GLES20.glActiveTexture(name)
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, id)
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST.toFloat())
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR.toFloat())
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE.toFloat())
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE.toFloat())
GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_LUMINANCE,
width, height, 0, GLES20.GL_LUMINANCE,
GLES20.GL_UNSIGNED_BYTE, null)
}
fun loadShader(type: Int, shaderCode: String?): Int {
val shader = GLES20.glCreateShader(type)
GLES20.glShaderSource(shader, shaderCode)
GLES20.glCompileShader(shader)
return shader
}
}
init {
val bb = ByteBuffer.allocateDirect(mXYZCoords.size * 4)
bb.order(ByteOrder.nativeOrder())
mVertexBuffer = bb.asFloatBuffer()
mVertexBuffer.put(mXYZCoords)
mVertexBuffer.position(0)
val tb = ByteBuffer.allocateDirect(mUVCoords.size * 4)
tb.order(ByteOrder.nativeOrder())
mTextureBuffer = tb.asFloatBuffer()
mTextureBuffer.put(mUVCoords)
mTextureBuffer.position(0)
val dlb = ByteBuffer.allocateDirect(mVertexIndex.size * 2)
dlb.order(ByteOrder.nativeOrder())
mDrawListBuffer = dlb.asShortBuffer()
mDrawListBuffer.put(mVertexIndex)
mDrawListBuffer.position(0)
}
}
override fun onFrame(frame: Frame) {
mRenderer.displayFrame(frame)
mView.requestRender()
}
override fun setStyle(key: String, value: String) {
if (STYLE_VIDEO_SCALE == key) {
if (STYLE_VIDEO_FIT == value) {
mRenderer.enableVideoFit(true)
} else if (STYLE_VIDEO_FILL == value) {
mRenderer.enableVideoFit(false)
}
}
}
override fun onVideoPropertiesChanged(videoEnabled: Boolean) {
mRenderer.disableVideo(!videoEnabled)
}
override fun getView(): View {
return mView
}
override fun onPause() {
mView.onPause()
}
override fun onResume() {
mView.onResume()
}
companion object {
private const val THUMBNAIL_SIZE = 90 //in dp
}
init {
mView.setEGLContextClientVersion(2)
mView.setEGLConfigChooser(8, 8, 8, 8, 16, 0)
mView.holder.setFormat(PixelFormat.TRANSLUCENT)
mRenderer = MyRenderer()
mView.setRenderer(mRenderer)
mView.renderMode = GLSurfaceView.RENDERMODE_WHEN_DIRTY
}
}

@ -0,0 +1,357 @@
package com.hmg.hmgDr.util
import android.content.Context
import android.content.res.Resources
import android.graphics.PixelFormat
import android.opengl.GLES20
import android.opengl.GLSurfaceView
import android.opengl.Matrix
import android.view.View
import com.opentok.android.BaseVideoRenderer
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.nio.FloatBuffer
import java.nio.ShortBuffer
import java.util.concurrent.locks.ReentrantLock
import javax.microedition.khronos.egl.EGLConfig
import javax.microedition.khronos.opengles.GL10
class ThumbnailCircleVideoRenderer(private val mContext: Context) : BaseVideoRenderer() {
private val mView: GLSurfaceView = GLSurfaceView(mContext)
private val mRenderer: MyRenderer
interface ThumbnailCircleVideoRendererMetadataListener {
fun onMetadataReady(metadata: ByteArray?)
}
fun setThumbnailCircleVideoRendererMetadataListener(metadataListener: ThumbnailCircleVideoRendererMetadataListener?) {
mRenderer.metadataListener = metadataListener
}
internal class MyRenderer : GLSurfaceView.Renderer {
var mTextureIds = IntArray(3)
var mScaleMatrix = FloatArray(16)
private val mVertexBuffer: FloatBuffer
private val mTextureBuffer: FloatBuffer
private val mDrawListBuffer: ShortBuffer
var mVideoFitEnabled = true
var mVideoDisabled = false
private val mVertexIndex = shortArrayOf(0, 1, 2, 0, 2, 3) // order to draw
// vertices
private val vertexShaderCode = """uniform mat4 uMVPMatrix;attribute vec4 aPosition;
attribute vec2 aTextureCoord;
varying vec2 vTextureCoord;
void main() {
gl_Position = uMVPMatrix * aPosition;
vTextureCoord = aTextureCoord;
}
"""
private val fragmentShaderCode = """precision mediump float;
uniform sampler2D Ytex;
uniform sampler2D Utex,Vtex;
uniform vec2 radiusDp;
varying vec2 vTextureCoord;
void main(void) {
float nx,ny,r,g,b,y,u,v;
mediump vec4 txl,ux,vx; nx=vTextureCoord[0];
ny=vTextureCoord[1];
y=texture2D(Ytex,vec2(nx,ny)).r;
u=texture2D(Utex,vec2(nx,ny)).r;
v=texture2D(Vtex,vec2(nx,ny)).r;
y=1.1643*(y-0.0625);
u=u-0.5;
v=v-0.5;
r=y+1.5958*v;
g=y-0.39173*u-0.81290*v;
b=y+2.017*u;
float radius = 0.5;
vec4 color0 = vec4(0.0, 0.0, 0.0, 0.0);
vec4 color1 = vec4(r, g, b, 1.0);
vec2 st = (gl_FragCoord.xy/radiusDp.xy); float dist = radius - distance(st,vec2(0.5));
float t = 1.0;
if (dist < 0.0) t = 0.0;
gl_FragColor = mix(color0, color1, t);
}
"""
var mFrameLock = ReentrantLock()
var mCurrentFrame: Frame? = null
private var mProgram = 0
private var mTextureWidth = 0
private var mTextureHeight = 0
private var mViewportWidth = 0
private var mViewportHeight = 0
override fun onSurfaceCreated(gl: GL10, config: EGLConfig) {
gl.glClearColor(0f, 0f, 0f, 1f)
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
val vertexShader = loadShader(GLES20.GL_VERTEX_SHADER,
vertexShaderCode)
val fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER,
fragmentShaderCode)
mProgram = GLES20.glCreateProgram() // create empty OpenGL ES
// Program
GLES20.glAttachShader(mProgram, vertexShader) // add the vertex
// shader to program
GLES20.glAttachShader(mProgram, fragmentShader) // add the fragment
// shader to
// program
GLES20.glLinkProgram(mProgram)
val positionHandle = GLES20.glGetAttribLocation(mProgram,
"aPosition")
val textureHandle = GLES20.glGetAttribLocation(mProgram,
"aTextureCoord")
GLES20.glVertexAttribPointer(positionHandle, COORDS_PER_VERTEX,
GLES20.GL_FLOAT, false, COORDS_PER_VERTEX * 4,
mVertexBuffer)
GLES20.glEnableVertexAttribArray(positionHandle)
GLES20.glVertexAttribPointer(textureHandle,
TEXTURECOORDS_PER_VERTEX, GLES20.GL_FLOAT, false,
TEXTURECOORDS_PER_VERTEX * 4, mTextureBuffer)
GLES20.glEnableVertexAttribArray(textureHandle)
GLES20.glUseProgram(mProgram)
var i = GLES20.glGetUniformLocation(mProgram, "Ytex")
GLES20.glUniform1i(i, 0) /* Bind Ytex to texture unit 0 */
i = GLES20.glGetUniformLocation(mProgram, "Utex")
GLES20.glUniform1i(i, 1) /* Bind Utex to texture unit 1 */
i = GLES20.glGetUniformLocation(mProgram, "Vtex")
GLES20.glUniform1i(i, 2) /* Bind Vtex to texture unit 2 */
val radiusDpLocation = GLES20.glGetUniformLocation(mProgram, "radiusDp")
val radiusDp = (Resources.getSystem().displayMetrics.density * THUMBNAIL_SIZE).toInt()
GLES20.glUniform2f(radiusDpLocation, radiusDp.toFloat(), radiusDp.toFloat())
mTextureWidth = 0
mTextureHeight = 0
}
fun setupTextures(frame: Frame) {
if (mTextureIds[0] != 0) {
GLES20.glDeleteTextures(3, mTextureIds, 0)
}
GLES20.glGenTextures(3, mTextureIds, 0)
val w = frame.width
val h = frame.height
val hw = w + 1 shr 1
val hh = h + 1 shr 1
initializeTexture(GLES20.GL_TEXTURE0, mTextureIds[0], w, h)
initializeTexture(GLES20.GL_TEXTURE1, mTextureIds[1], hw, hh)
initializeTexture(GLES20.GL_TEXTURE2, mTextureIds[2], hw, hh)
mTextureWidth = frame.width
mTextureHeight = frame.height
}
fun updateTextures(frame: Frame) {
val width = frame.width
val height = frame.height
val half_width = width + 1 shr 1
val half_height = height + 1 shr 1
val y_size = width * height
val uv_size = half_width * half_height
val bb = frame.buffer
// If we are reusing this frame, make sure we reset position and
// limit
bb.clear()
if (bb.remaining() == y_size + uv_size * 2) {
bb.position(0)
GLES20.glPixelStorei(GLES20.GL_UNPACK_ALIGNMENT, 1)
GLES20.glPixelStorei(GLES20.GL_PACK_ALIGNMENT, 1)
GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureIds[0])
GLES20.glTexSubImage2D(GLES20.GL_TEXTURE_2D, 0, 0, 0, width,
height, GLES20.GL_LUMINANCE, GLES20.GL_UNSIGNED_BYTE,
bb)
bb.position(y_size)
GLES20.glActiveTexture(GLES20.GL_TEXTURE1)
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureIds[1])
GLES20.glTexSubImage2D(GLES20.GL_TEXTURE_2D, 0, 0, 0,
half_width, half_height, GLES20.GL_LUMINANCE,
GLES20.GL_UNSIGNED_BYTE, bb)
bb.position(y_size + uv_size)
GLES20.glActiveTexture(GLES20.GL_TEXTURE2)
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureIds[2])
GLES20.glTexSubImage2D(GLES20.GL_TEXTURE_2D, 0, 0, 0,
half_width, half_height, GLES20.GL_LUMINANCE,
GLES20.GL_UNSIGNED_BYTE, bb)
} else {
mTextureWidth = 0
mTextureHeight = 0
}
}
override fun onSurfaceChanged(gl: GL10, width: Int, height: Int) {
GLES20.glViewport(0, 0, width, height)
mViewportWidth = width
mViewportHeight = height
}
var metadataListener: ThumbnailCircleVideoRendererMetadataListener? = null
override fun onDrawFrame(gl: GL10) {
gl.glClearColor(0f, 0f, 0f, 0f)
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
mFrameLock.lock()
if (mCurrentFrame != null && !mVideoDisabled) {
GLES20.glUseProgram(mProgram)
if (mTextureWidth != mCurrentFrame!!.width
|| mTextureHeight != mCurrentFrame!!.height) {
setupTextures(mCurrentFrame!!)
}
updateTextures(mCurrentFrame!!)
Matrix.setIdentityM(mScaleMatrix, 0)
var scaleX = 1.0f
var scaleY = 1.0f
val ratio = (mCurrentFrame!!.width.toFloat()
/ mCurrentFrame!!.height)
val vratio = mViewportWidth.toFloat() / mViewportHeight
if (mVideoFitEnabled) {
if (ratio > vratio) {
scaleY = vratio / ratio
} else {
scaleX = ratio / vratio
}
} else {
if (ratio < vratio) {
scaleY = vratio / ratio
} else {
scaleX = ratio / vratio
}
}
Matrix.scaleM(mScaleMatrix, 0,
scaleX * if (mCurrentFrame!!.isMirroredX) -1.0f else 1.0f,
scaleY, 1f)
metadataListener?.onMetadataReady(mCurrentFrame!!.metadata)
val mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram,
"uMVPMatrix")
GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false,
mScaleMatrix, 0)
GLES20.glDrawElements(GLES20.GL_TRIANGLES, mVertexIndex.size,
GLES20.GL_UNSIGNED_SHORT, mDrawListBuffer)
} else {
//black frame when video is disabled
gl.glClearColor(0f, 0f, 0f, 1f)
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
}
mFrameLock.unlock()
}
fun displayFrame(frame: Frame?) {
mFrameLock.lock()
if (mCurrentFrame != null) {
mCurrentFrame!!.recycle()
}
mCurrentFrame = frame
mFrameLock.unlock()
}
fun disableVideo(b: Boolean) {
mFrameLock.lock()
mVideoDisabled = b
if (mVideoDisabled) {
if (mCurrentFrame != null) {
mCurrentFrame!!.recycle()
}
mCurrentFrame = null
}
mFrameLock.unlock()
}
fun enableVideoFit(enableVideoFit: Boolean) {
mVideoFitEnabled = enableVideoFit
}
companion object {
// number of coordinates per vertex in this array
const val COORDS_PER_VERTEX = 3
const val TEXTURECOORDS_PER_VERTEX = 2
var mXYZCoords = floatArrayOf(
-1.0f, 1.0f, 0.0f, // top left
-1.0f, -1.0f, 0.0f, // bottom left
1.0f, -1.0f, 0.0f, // bottom right
1.0f, 1.0f, 0.0f // top right
)
var mUVCoords = floatArrayOf(0f, 0f, 0f, 1f, 1f, 1f, 1f, 0f)
fun initializeTexture(name: Int, id: Int, width: Int, height: Int) {
GLES20.glActiveTexture(name)
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, id)
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST.toFloat())
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR.toFloat())
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE.toFloat())
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE.toFloat())
GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_LUMINANCE,
width, height, 0, GLES20.GL_LUMINANCE,
GLES20.GL_UNSIGNED_BYTE, null)
}
fun loadShader(type: Int, shaderCode: String?): Int {
val shader = GLES20.glCreateShader(type)
GLES20.glShaderSource(shader, shaderCode)
GLES20.glCompileShader(shader)
return shader
}
}
init {
val bb = ByteBuffer.allocateDirect(mXYZCoords.size * 4)
bb.order(ByteOrder.nativeOrder())
mVertexBuffer = bb.asFloatBuffer()
mVertexBuffer.put(mXYZCoords)
mVertexBuffer.position(0)
val tb = ByteBuffer.allocateDirect(mUVCoords.size * 4)
tb.order(ByteOrder.nativeOrder())
mTextureBuffer = tb.asFloatBuffer()
mTextureBuffer.put(mUVCoords)
mTextureBuffer.position(0)
val dlb = ByteBuffer.allocateDirect(mVertexIndex.size * 2)
dlb.order(ByteOrder.nativeOrder())
mDrawListBuffer = dlb.asShortBuffer()
mDrawListBuffer.put(mVertexIndex)
mDrawListBuffer.position(0)
}
}
override fun onFrame(frame: Frame) {
mRenderer.displayFrame(frame)
mView.requestRender()
}
override fun setStyle(key: String, value: String) {
if (STYLE_VIDEO_SCALE == key) {
if (STYLE_VIDEO_FIT == value) {
mRenderer.enableVideoFit(true)
} else if (STYLE_VIDEO_FILL == value) {
mRenderer.enableVideoFit(false)
}
}
}
override fun onVideoPropertiesChanged(videoEnabled: Boolean) {
mRenderer.disableVideo(!videoEnabled)
}
override fun getView(): View {
return mView
}
override fun onPause() {
mView.onPause()
}
override fun onResume() {
mView.onResume()
}
companion object {
private const val THUMBNAIL_SIZE = 90 //in dp
}
init {
mView.setEGLContextClientVersion(2)
mView.setEGLConfigChooser(8, 8, 8, 8, 16, 0)
mView.holder.setFormat(PixelFormat.TRANSLUCENT)
mRenderer = MyRenderer()
mView.setRenderer(mRenderer)
mView.renderMode = GLSurfaceView.RENDERMODE_WHEN_DIRTY
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid
android:color="@color/remoteBackground"/>
<stroke android:width="2dp" android:color="@color/text_color" />
<size
android:width="120dp"
android:height="120dp"/>
</shape>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

@ -1,16 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M8,8l-2,0l0,9l11,0l0,-2l-9,0z"/>
<path
android:fillColor="@android:color/white"
android:pathData="M22,3H10v10h12V3zM20,11h-8V7h8V11z"/>
<path
android:fillColor="@android:color/white"
android:pathData="M4,12l-2,0l0,9l11,0l0,-2l-9,0z"/>
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M19,13H5v-2h14v2z"/>
</vector>

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/text_color" />
<stroke
android:width="3dp"
android:color="@color/text_color" />
<corners android:radius="10dp" />
<padding
android:bottom="0dp"
android:left="0dp"
android:right="0dp"
android:top="0dp" />
</shape>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

@ -1,15 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/video_call_ll"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/text_color"
android:orientation="vertical">
<RelativeLayout
android:id="@+id/layout_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/padding_space_big">
android:padding="@dimen/padding_space_medium"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/patient_name"
@ -30,13 +34,14 @@
android:padding="@dimen/padding_space_small">
<Chronometer
android:id="@+id/cmTimer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20sp"
tools:text="25:45"
android:padding="4dp"
android:textColor="@color/white"
android:textSize="16sp"
android:textStyle="bold"
android:id="@+id/cmTimer" />
tools:text="25:45" />
</FrameLayout>
@ -46,47 +51,44 @@
android:id="@+id/activity_clingo_video_call"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
tools:context=".ui.VideoCallActivity">
app:layout_constraintBottom_toTopOf="@id/control_panel"
app:layout_constraintTop_toBottomOf="@+id/layout_name">
<RelativeLayout
android:id="@+id/layout_mini"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true"
android:gravity="center_horizontal"
android:keepScreenOn="true">
<LinearLayout
android:id="@+id/subscriberview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:orientation="horizontal" />
<RelativeLayout
android:id="@+id/publisherview"
android:layout_width="150dp"
android:layout_height="200dp"
android:layout_alignParentRight="true"
android:layout_alignParentBottom="true"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingBottom="@dimen/activity_vertical_margin" />
android:layout_height="wrap_content"
android:background="@color/remoteBackground"
android:alpha="0.5"
android:visibility="gone">
<ImageButton
android:id="@+id/ic_mini"
style="@style/Widget.MaterialComponents.Button.Icon"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_margin="@dimen/padding_space_medium"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:background="@null"
android:src="@drawable/ic_mini" />
</RelativeLayout>
<RelativeLayout
<FrameLayout
android:id="@+id/remote_video_view_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/layout_mini"
android:background="@color/remoteBackground">
<ImageView
android:id="@+id/remote_video_view_icon"
android:layout_width="@dimen/remote_back_icon_size"
android:layout_height="@dimen/remote_back_icon_size"
android:layout_centerInParent="true"
android:layout_gravity="center"
android:src="@drawable/video_off_fill" />
</RelativeLayout>
</FrameLayout>
<FrameLayout
android:id="@+id/local_video_view_container"
@ -95,10 +97,10 @@
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_marginTop="@dimen/local_preview_margin_top"
android:layout_marginEnd="@dimen/local_preview_margin_right"
android:background="@color/localBackground">
android:layout_marginEnd="@dimen/local_preview_margin_top">
<ImageView
android:id="@+id/local_video_view_icon"
android:layout_width="@dimen/local_back_icon_size"
android:layout_height="@dimen/local_back_icon_size"
android:layout_gravity="center"
@ -106,76 +108,90 @@
android:src="@drawable/video_off_fill" />
</FrameLayout>
<ImageView
android:id="@+id/btn_switch_camera"
android:layout_width="@dimen/video_icon_size"
android:layout_height="@dimen/video_icon_size"
android:layout_centerVertical="true"
android:layout_below="@+id/local_video_view_container"
android:layout_marginTop="@dimen/local_preview_margin_top"
android:layout_marginEnd="@dimen/local_preview_margin_right"
android:layout_alignParentEnd="true"
android:onClick="onSwitchCameraClicked"
android:scaleType="centerCrop"
android:src="@drawable/flip_enabled" />
<FrameLayout
android:id="@+id/thumbnail_container"
android:layout_width="90dp"
android:layout_height="90dp"
android:visibility="gone"
android:layout_alignParentBottom="true"
android:layout_marginBottom="16dp"
android:layout_marginStart="16dp"
/>
</RelativeLayout>
<RelativeLayout
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/control_panel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/padding_space_big_2">
android:padding="@dimen/padding_space_big"
app:layout_constraintBottom_toBottomOf="parent">
<ImageView
android:id="@+id/btn_call"
android:layout_width="@dimen/video_icon_size"
android:layout_height="@dimen/video_icon_size"
android:layout_alignParentEnd="true"
android:onClick="onCallClicked"
android:scaleType="centerCrop"
android:src="@drawable/call" />
android:src="@drawable/call"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/btn_minimize"
android:layout_width="@dimen/video_icon_size"
android:layout_height="@dimen/video_icon_size"
android:layout_alignParentStart="true"
android:onClick="onMinimizedClicked"
android:layout_marginEnd="@dimen/padding_space_small"
android:scaleType="centerCrop"
android:src="@drawable/ic_mini" />
android:src="@drawable/reducing"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/btn_camera"
android:layout_width="@dimen/video_icon_size"
android:layout_height="@dimen/video_icon_size"
android:layout_toEndOf="@id/btn_minimize"
android:onClick="onCameraClicked"
android:layout_marginStart="@dimen/padding_space_medium"
android:scaleType="centerCrop"
android:layout_marginEnd="@dimen/padding_space_small"
android:src="@drawable/video_enabled" />
android:src="@drawable/video_enabled"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/btn_minimize"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/btn_mic"
android:layout_width="@dimen/video_icon_size"
android:layout_height="@dimen/video_icon_size"
android:layout_toEndOf="@id/btn_camera"
android:onClick="onMicClicked"
android:layout_marginEnd="@dimen/padding_space_small"
android:layout_marginStart="@dimen/padding_space_medium"
android:scaleType="centerCrop"
android:src="@drawable/mic_enabled" />
android:src="@drawable/mic_enabled"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/btn_camera"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/btn_specker"
android:id="@+id/btn_switch_camera"
android:layout_width="@dimen/video_icon_size"
android:layout_height="@dimen/video_icon_size"
android:layout_toEndOf="@id/btn_mic"
android:onClick="onSpeckerClicked"
android:layout_marginStart="@dimen/padding_space_medium"
android:scaleType="centerCrop"
android:layout_marginEnd="@dimen/padding_space_small"
android:src="@drawable/audio_enabled" />
android:src="@drawable/camera_back"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/btn_mic"
app:layout_constraintTop_toTopOf="parent" />
</RelativeLayout>
</LinearLayout>
<ImageView
android:id="@+id/btn_specker"
android:layout_width="@dimen/video_icon_size"
android:layout_height="@dimen/video_icon_size"
android:layout_marginStart="@dimen/padding_space_medium"
android:scaleType="centerCrop"
android:src="@drawable/audio_enabled"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/btn_mic"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

@ -5,6 +5,10 @@
<color name="colorAccent">#fc3850</color>
<color name="colorProgressBarBackground">#e4e9f2</color>
<color name="transparent">#80757575</color>
<color name="transparent_full_opacity">#00ffffff</color>
<!-- Chat Activity -->
<color name="localBackground">#827b92</color>
<color name="remoteBackground">#484258</color>

@ -3,27 +3,33 @@
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
<dimen name="local_preview_margin_top">28dp</dimen>
<dimen name="local_preview_margin_small">12dp</dimen>
<dimen name="local_preview_margin_right">24dp</dimen>
<!-- buttons -->
<dimen name="call_button_size">60dp</dimen>
<dimen name="other_button_size">54dp</dimen>
<dimen name="video_icon_size">64dp</dimen>
<dimen name="video_icon_size">52dp</dimen>
<dimen name="video_icon_size_small">24dp</dimen>
<!-- buttons -->
<dimen name="control_bottom_margin">24dp</dimen>
<dimen name="control_bottom_horizontal_margin">25dp</dimen>
<dimen name="local_preview_width">88dp</dimen>
<dimen name="local_preview_width_small">40dp</dimen>
<dimen name="local_preview_height">117dp</dimen>
<dimen name="local_preview_height_small">50dp</dimen>
<dimen name="local_back_icon_size">50dp</dimen>
<dimen name="local_back_icon_size_small">25dp</dimen>
<dimen name="remote_back_icon_size">100dp</dimen>
<dimen name="remote_back_icon_size_small">40dp</dimen>
<dimen name="remote_back_icon_margin_bottom">90dp</dimen>
<!-- text-->
<dimen name="text_size_small">14sp</dimen>
<dimen name="text_size_medium">16sp</dimen>
<dimen name="text_size_big">24sp</dimen>
<dimen name="text_size_big">22sp</dimen>
<!-- padding/margin-->
<dimen name="padding_space_small">4dp</dimen>

@ -4,5 +4,7 @@
<string name="remaining_ar">الوقت المتبقي بالثانيه: </string>
<string name="setting">Settings</string>
<string name="cancel">Cancel</string>
<!-- TODO: Remove or change this placeholder text -->
<string name="hello_blank_fragment">Hello blank fragment</string>
</resources>

@ -16,4 +16,21 @@
<item name="android:windowFullscreen">true</item>
<item name="android:windowContentOverlay">@null</item>
</style>
<style name="dialogTheme" parent="Theme.AppCompat.DayNight.Dialog.Alert">
<!-- title encapsulating main part (backgroud) of custom alertdialog -->
<item name="android:windowFrame">@null</item>
<!-- turn off any drawable used to draw a frame on the window -->
<item name="android:windowBackground">@null</item>
<!-- turn off any drawable used to draw a frame on the window -->
<item name="android:windowIsFloating">true</item>
<!-- float the window so it does not fill the screen -->
<item name="android:windowNoTitle">true</item>
<!-- remove the title bar we make our own-->
<item name="android:windowContentOverlay">@null</item>
<!-- remove the shadow from under the title bar -->
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">match_parent</item>
</style>
</resources>

@ -65,6 +65,7 @@ target 'Runner' do
pod 'Flutter', :path => 'Flutter'
pod 'OpenTok'
pod 'Alamofire', '~> 5.2'
pod 'AADraggableView'
# Plugin Pods
# Prepare symlinks folder. We use symlinks to avoid having Podfile.lock

@ -9,12 +9,16 @@
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
29211E4225C172B700DD740D /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 29211E4125C172B700DD740D /* GoogleService-Info.plist */; };
300790FA266FB14B0052174C /* VCEmbeder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 300790F9266FB14B0052174C /* VCEmbeder.swift */; };
300790FC26710CAB0052174C /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 300790FB26710CAB0052174C /* Extensions.swift */; };
30F70E6C266F56FD005D8F8E /* MainAppViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30F70E6B266F56FD005D8F8E /* MainAppViewController.swift */; };
30F70E6F266F6509005D8F8E /* VideoCallRequestParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30F70E6E266F6509005D8F8E /* VideoCallRequestParameters.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
9CE61EBD24AB366E008D68DD /* VideoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CE61EBC24AB366E008D68DD /* VideoViewController.swift */; };
9CE61EBD24AB366E008D68DD /* VideoCallViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CE61EBC24AB366E008D68DD /* VideoCallViewController.swift */; };
9CE61ECD24ADBB4C008D68DD /* ICallProtocoll.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CE61ECC24ADBB4C008D68DD /* ICallProtocoll.swift */; };
B650DC3076E9D70CB188286A /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 93A5F83B23AB032D1E096663 /* Pods_Runner.framework */; };
/* End PBXBuildFile section */
@ -37,6 +41,10 @@
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
29211CD725C165D600DD740D /* RunnerRelease.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerRelease.entitlements; sourceTree = "<group>"; };
29211E4125C172B700DD740D /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
300790F9266FB14B0052174C /* VCEmbeder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VCEmbeder.swift; sourceTree = "<group>"; };
300790FB26710CAB0052174C /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = "<group>"; };
30F70E6B266F56FD005D8F8E /* MainAppViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainAppViewController.swift; sourceTree = "<group>"; };
30F70E6E266F6509005D8F8E /* VideoCallRequestParameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoCallRequestParameters.swift; sourceTree = "<group>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
@ -50,7 +58,7 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
9CE61EBC24AB366E008D68DD /* VideoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoViewController.swift; sourceTree = "<group>"; };
9CE61EBC24AB366E008D68DD /* VideoCallViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoCallViewController.swift; sourceTree = "<group>"; };
9CE61ECC24ADBB4C008D68DD /* ICallProtocoll.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ICallProtocoll.swift; sourceTree = "<group>"; };
9D4B7DB43C6A6C849D2387CE /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
E698D7B14B12DF768FE47A1A /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
@ -68,6 +76,31 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
300790F8266FB0F10052174C /* helpers */ = {
isa = PBXGroup;
children = (
300790F9266FB14B0052174C /* VCEmbeder.swift */,
300790FB26710CAB0052174C /* Extensions.swift */,
);
name = helpers;
sourceTree = "<group>";
};
30F70E6A266F56C9005D8F8E /* controllers */ = {
isa = PBXGroup;
children = (
30F70E6B266F56FD005D8F8E /* MainAppViewController.swift */,
);
name = controllers;
sourceTree = "<group>";
};
30F70E6D266F64F8005D8F8E /* Models */ = {
isa = PBXGroup;
children = (
30F70E6E266F6509005D8F8E /* VideoCallRequestParameters.swift */,
);
name = Models;
sourceTree = "<group>";
};
7D66D387293CE5376A07EC5F /* Pods */ = {
isa = PBXGroup;
children = (
@ -111,6 +144,9 @@
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
300790F8266FB0F10052174C /* helpers */,
30F70E6D266F64F8005D8F8E /* Models */,
30F70E6A266F56C9005D8F8E /* controllers */,
29211E4125C172B700DD740D /* GoogleService-Info.plist */,
29211CD725C165D600DD740D /* RunnerRelease.entitlements */,
97C146FA1CF9000F007C117D /* Main.storyboard */,
@ -122,7 +158,7 @@
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
9CE61EBC24AB366E008D68DD /* VideoViewController.swift */,
9CE61EBC24AB366E008D68DD /* VideoCallViewController.swift */,
9CE61ECC24ADBB4C008D68DD /* ICallProtocoll.swift */,
);
path = Runner;
@ -290,10 +326,14 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
300790FA266FB14B0052174C /* VCEmbeder.swift in Sources */,
30F70E6F266F6509005D8F8E /* VideoCallRequestParameters.swift in Sources */,
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
9CE61EBD24AB366E008D68DD /* VideoViewController.swift in Sources */,
300790FC26710CAB0052174C /* Extensions.swift in Sources */,
9CE61EBD24AB366E008D68DD /* VideoCallViewController.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
9CE61ECD24ADBB4C008D68DD /* ICallProtocoll.swift in Sources */,
30F70E6C266F56FD005D8F8E /* MainAppViewController.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

@ -27,6 +27,8 @@
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
@ -36,8 +38,8 @@
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
</Testables>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
@ -59,6 +61,8 @@
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"

@ -22,34 +22,8 @@ import OpenTok
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
let videoCallChannel = FlutterMethodChannel(name: "Dr.cloudSolution/videoCall",
binaryMessenger: controller.binaryMessenger)
videoCallChannel.setMethodCallHandler({
(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
self.result = result
switch call.method {
case "openVideoCall":
do {
let arguments = call.arguments as? NSDictionary
let kApiKey = arguments!["kApiKey"] as? String
let kSessionId = arguments!["kSessionId"] as? String
let kToken = arguments!["kToken"] as? String
let appLang = arguments!["appLang"] as? String
let vC_ID = arguments!["VC_ID"] as? Int
let tokenID = arguments!["TokenID"] as? String
let generalId = arguments!["generalId"] as? String
let doctorId = arguments!["DoctorId"] as? Int
let baseUrl = arguments!["baseUrl"] as? String
self.openVideoChat(result: result,kApiKey: kApiKey!,kSessionId:kSessionId!,kToken: kToken!, appLang: appLang!, vC_ID: vC_ID!,tokenID: tokenID!,generalId: generalId!,doctorId: doctorId!, baseUrl: baseUrl!)
}
default:
result(FlutterMethodNotImplemented)
}
})
GeneratedPluginRegistrant.register(with: self)
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
@ -61,7 +35,7 @@ import OpenTok
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let identifier = "ViewControllerNav"
let navVC = storyboard.instantiateViewController(withIdentifier: identifier) as! UINavigationController
let videoVC = navVC.viewControllers.first as! ViewController
let videoVC = navVC.viewControllers.first as! VideoCallViewController
videoVC.kApiKey=kApiKey
videoVC.kSessionId=kSessionId
videoVC.kToken=kToken
@ -75,6 +49,6 @@ import OpenTok
navVC.modalPresentationStyle = .fullScreen
window.rootViewController?.present(navVC, animated: true, completion: nil)
}
}

@ -1,6 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
"author" : "xcode",
"version" : 1
}
}
}

@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "camera_back.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "camera_front.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "end_call.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "expand.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "Group 8024.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "hide_video_icon.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 906 B

@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "mic_mute.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "Group 8020.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "video_mute.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "Group 8022.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

@ -1,296 +1,366 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16097" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="18122" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<device id="retina6_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<!--Main App View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<viewController id="BYZ-38-t0r" customClass="MainAppViewController" customModule="Runner" customModuleProvider="target" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<rect key="frame" x="0.0" y="0.0" width="428" height="926"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-45" y="1436"/>
<point key="canvasLocation" x="-1535" y="2239"/>
</scene>
<!--View Controller-->
<!--Video Call View Controller-->
<scene sceneID="L7v-n1-KvF">
<objects>
<viewController id="t2c-G5-7AE" customClass="ViewController" customModule="Runner" customModuleProvider="target" sceneMemberID="viewController">
<viewController storyboardIdentifier="videoCall" id="t2c-G5-7AE" customClass="VideoCallViewController" customModule="Runner" customModuleProvider="target" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="BIa-yr-ZMY"/>
<viewControllerLayoutGuide type="bottom" id="VIH-1T-Ife"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="PoR-7r-yNe">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<rect key="frame" x="0.0" y="0.0" width="428" height="926"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="6C5-o5-R1n" userLabel="controlButtonSpacer">
<rect key="frame" x="14" y="844" width="139" height="30"/>
<constraints>
<constraint firstAttribute="height" constant="30" id="0mU-Ko-Up3"/>
<constraint firstAttribute="width" constant="139" id="DNz-Ig-XOS"/>
</constraints>
</view>
<view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="yS9-HT-vp8" userLabel="controlButtonSpacer">
<rect key="frame" x="265" y="844" width="140" height="30"/>
<constraints>
<constraint firstAttribute="width" constant="140" id="Alm-7E-yeU"/>
<constraint firstAttribute="height" constant="30" id="amS-in-0UH"/>
</constraints>
</view>
<view contentMode="scaleToFill" restorationIdentifier="VideoCallNavigationViewController" translatesAutoresizingMaskIntoConstraints="NO" id="eOT-Jr-GUl" userLabel="remoteVideo">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<rect key="frame" x="0.0" y="0.0" width="428" height="926"/>
<color key="backgroundColor" white="0.0" alpha="0.98999999999999999" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="videoMutedIndicator" translatesAutoresizingMaskIntoConstraints="NO" id="CL1-2v-m1L" userLabel="remoteVideoMutedIndicator">
<rect key="frame" x="147" y="398" width="120" height="100"/>
<rect key="frame" x="160.66666666666666" y="418.33333333333331" width="106.99999999999997" height="89.333333333333314"/>
<constraints>
<constraint firstAttribute="width" constant="120" id="eon-3Z-74P"/>
<constraint firstAttribute="height" constant="100" id="iyK-hL-H3m"/>
<constraint firstAttribute="width" secondItem="CL1-2v-m1L" secondAttribute="height" multiplier="6:5" id="xqE-6Q-WAu"/>
</constraints>
</imageView>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="0ZP-s2-kM5" userLabel="localVideo">
<rect key="frame" x="270.5" y="138" width="103.5" height="224"/>
</view>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="localVideoMutedBg" translatesAutoresizingMaskIntoConstraints="NO" id="A0G-cT-yji" userLabel="localVideoMutedBg">
<rect key="frame" x="215" y="138" width="159" height="224"/>
</imageView>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="sK0-tB-H9u" userLabel="controlButtons">
<rect key="frame" x="0.0" y="740" width="414" height="80"/>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="O5s-CJ-RFv">
<rect key="frame" x="0.0" y="100" width="428" height="731"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="NUO-2p-pzL" userLabel="videoMuteButton">
<rect key="frame" x="29" y="20.5" width="39" height="39"/>
<constraints>
<constraint firstAttribute="width" constant="39" id="CWU-fT-9gx"/>
<constraint firstAttribute="height" constant="39" id="nsM-Xv-O2N"/>
</constraints>
<state key="normal" title="Button" image="speakerOff"/>
<state key="selected" image="speakerOffSelected"/>
<connections>
<action selector="didClickSpeakerButton:" destination="t2c-G5-7AE" eventType="touchUpInside" id="XHg-oo-TWD"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="N2h-FX-kF5" userLabel="muteButton">
<rect key="frame" x="98" y="20.5" width="39" height="39"/>
<view clipsSubviews="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="XHG-Db-3Px" customClass="AADraggableView" customModule="AADraggableView">
<rect key="frame" x="288" y="10" width="130" height="182"/>
<subviews>
<view clipsSubviews="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="0ZP-s2-kM5" userLabel="localVideo">
<rect key="frame" x="0.0" y="0.0" width="130" height="182"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius">
<integer key="value" value="5"/>
</userDefinedRuntimeAttribute>
<userDefinedRuntimeAttribute type="number" keyPath="layer.borderWidth">
<real key="value" value="0.5"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</view>
<view clipsSubviews="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="6y1-an-W7F">
<rect key="frame" x="0.0" y="0.0" width="130" height="182"/>
<subviews>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="videoMutedIndicator" translatesAutoresizingMaskIntoConstraints="NO" id="FEH-bC-Hnh" userLabel="localVideoMutedIndicator">
<rect key="frame" x="47" y="76" width="36" height="30"/>
<constraints>
<constraint firstAttribute="height" constant="30" id="1iv-w5-GDT"/>
<constraint firstAttribute="width" constant="36" id="AwX-vQ-Ynn"/>
</constraints>
</imageView>
</subviews>
<color key="backgroundColor" white="0.33333333333333331" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="FEH-bC-Hnh" firstAttribute="centerX" secondItem="6y1-an-W7F" secondAttribute="centerX" id="dtO-VP-8qV"/>
<constraint firstItem="FEH-bC-Hnh" firstAttribute="centerY" secondItem="6y1-an-W7F" secondAttribute="centerY" id="lpW-Vr-bH2"/>
</constraints>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius">
<integer key="value" value="5"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</view>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="width" constant="39" id="Nv9-dR-SYL"/>
<constraint firstAttribute="height" constant="39" id="lFc-rS-c6B"/>
<constraint firstItem="0ZP-s2-kM5" firstAttribute="top" secondItem="XHG-Db-3Px" secondAttribute="top" id="E5A-Yq-Ods"/>
<constraint firstAttribute="trailing" secondItem="6y1-an-W7F" secondAttribute="trailing" id="I0l-x2-rGO"/>
<constraint firstAttribute="width" secondItem="XHG-Db-3Px" secondAttribute="height" multiplier="1:1.4" id="M3V-vM-m0V"/>
<constraint firstAttribute="bottom" secondItem="6y1-an-W7F" secondAttribute="bottom" id="T0D-IE-19P"/>
<constraint firstItem="6y1-an-W7F" firstAttribute="top" secondItem="XHG-Db-3Px" secondAttribute="top" id="T8b-Nd-jdR"/>
<constraint firstItem="6y1-an-W7F" firstAttribute="leading" secondItem="XHG-Db-3Px" secondAttribute="leading" id="brg-bp-MME"/>
<constraint firstAttribute="bottom" secondItem="0ZP-s2-kM5" secondAttribute="bottom" id="hso-Od-7QF"/>
<constraint firstItem="0ZP-s2-kM5" firstAttribute="leading" secondItem="XHG-Db-3Px" secondAttribute="leading" id="k5R-AM-3J6"/>
<constraint firstAttribute="trailing" secondItem="0ZP-s2-kM5" secondAttribute="trailing" id="z8V-qh-Rzq"/>
</constraints>
<state key="normal" title="Button" image="muteButton"/>
<state key="selected" image="muteButtonSelected"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="padding">
<integer key="value" value="20"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</view>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="4S7-45-trL">
<rect key="frame" x="0.0" y="0.0" width="428" height="731"/>
<connections>
<action selector="didClickMuteButton:" destination="t2c-G5-7AE" eventType="touchUpInside" id="7lr-uj-db1"/>
<action selector="btnSwipeVideoTapped:" destination="t2c-G5-7AE" eventType="touchUpInside" id="7AQ-6d-4iR"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="ygd-al-G4F" userLabel="switchCameraButton">
<rect key="frame" x="352" y="20.5" width="39" height="39"/>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="4S7-45-trL" firstAttribute="leading" secondItem="O5s-CJ-RFv" secondAttribute="leading" id="AMe-9q-Xk9"/>
<constraint firstAttribute="trailing" secondItem="4S7-45-trL" secondAttribute="trailing" id="CX4-Rs-rep"/>
<constraint firstAttribute="trailing" secondItem="XHG-Db-3Px" secondAttribute="trailing" constant="10" id="Ncy-sD-qeL"/>
<constraint firstAttribute="bottom" secondItem="4S7-45-trL" secondAttribute="bottom" id="iJ9-pg-VXQ"/>
<constraint firstItem="XHG-Db-3Px" firstAttribute="top" secondItem="O5s-CJ-RFv" secondAttribute="top" constant="10" id="nM8-B7-soj"/>
<constraint firstItem="4S7-45-trL" firstAttribute="top" secondItem="O5s-CJ-RFv" secondAttribute="top" id="sNK-WK-tqV"/>
</constraints>
</view>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="WPH-QE-Fvp">
<rect key="frame" x="0.0" y="0.0" width="428" height="926"/>
<connections>
<action selector="btnOnScreenTapped:" destination="t2c-G5-7AE" eventType="touchUpInside" id="jDS-QU-1VX"/>
</connections>
</button>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="sK0-tB-H9u" userLabel="controlButtons">
<rect key="frame" x="0.0" y="831" width="428" height="95"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" spacing="10" translatesAutoresizingMaskIntoConstraints="NO" id="r9w-8l-Zxj">
<rect key="frame" x="15" y="15" width="230" height="50"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="UZ6-EY-a59" userLabel="minMax">
<rect key="frame" x="0.0" y="0.0" width="50" height="50"/>
<constraints>
<constraint firstAttribute="width" secondItem="UZ6-EY-a59" secondAttribute="height" multiplier="1:1" id="Ula-Qc-wJE"/>
</constraints>
<state key="normal" title="Button" image="float_video"/>
<state key="selected" image="expand_video"/>
<connections>
<action selector="btnMinimizeTapped:" destination="t2c-G5-7AE" eventType="touchUpInside" id="xul-cC-cbm"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleAspectFit" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="NUO-2p-pzL" userLabel="videoMuteButton">
<rect key="frame" x="60" y="0.0" width="50" height="50"/>
<constraints>
<constraint firstAttribute="width" secondItem="NUO-2p-pzL" secondAttribute="height" multiplier="1:1" id="KGm-Yp-MqK"/>
</constraints>
<state key="normal" image="video_unmute"/>
<state key="selected" image="video_mute"/>
<connections>
<action selector="didClickVideoMuteButton:" destination="t2c-G5-7AE" eventType="touchUpInside" id="nk9-nS-UDE"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="N2h-FX-kF5" userLabel="muteButton">
<rect key="frame" x="120" y="0.0" width="50" height="50"/>
<constraints>
<constraint firstAttribute="width" secondItem="N2h-FX-kF5" secondAttribute="height" multiplier="1:1" id="hI1-G0-K3C"/>
</constraints>
<state key="normal" image="mic_unmute"/>
<state key="selected" image="mic_mute"/>
<connections>
<action selector="didClickMuteButton:" destination="t2c-G5-7AE" eventType="touchUpInside" id="7lr-uj-db1"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" selected="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="lar-xK-rhR" userLabel="switchCamera">
<rect key="frame" x="180" y="0.0" width="50" height="50"/>
<constraints>
<constraint firstAttribute="width" secondItem="lar-xK-rhR" secondAttribute="height" multiplier="1:1" id="Wpc-dx-kpF"/>
</constraints>
<state key="normal" image="camera_back"/>
<state key="selected" image="camera_front"/>
<connections>
<action selector="didClickSwitchCameraButton:" destination="t2c-G5-7AE" eventType="touchUpInside" id="S3g-7j-Ca0"/>
</connections>
</button>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="width" constant="39" id="EHa-e5-BfD"/>
<constraint firstAttribute="height" constant="39" id="KEK-0n-HJw"/>
<constraint firstAttribute="height" constant="50" id="3L0-3h-Bxg"/>
<constraint firstItem="lar-xK-rhR" firstAttribute="width" secondItem="lar-xK-rhR" secondAttribute="height" multiplier="1:1" id="R46-j0-zPf"/>
<constraint firstItem="UZ6-EY-a59" firstAttribute="height" secondItem="NUO-2p-pzL" secondAttribute="height" id="kK6-uF-bf6"/>
<constraint firstItem="N2h-FX-kF5" firstAttribute="height" secondItem="NUO-2p-pzL" secondAttribute="height" id="o4R-49-iMe"/>
<constraint firstAttribute="height" constant="30" id="y90-hA-p5Z"/>
</constraints>
<state key="normal" title="Button" image="switchCameraButton"/>
<state key="selected" image="switchCameraButtonSelected"/>
<connections>
<action selector="didClickSwitchCameraButton:" destination="t2c-G5-7AE" eventType="touchUpInside" id="TBa-Av-IXy"/>
</connections>
</button>
<variation key="default">
<mask key="constraints">
<exclude reference="y90-hA-p5Z"/>
</mask>
</variation>
</stackView>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="AoF-Up-Yu5" userLabel="hangUpButton">
<rect key="frame" x="174" y="4.5" width="71" height="71"/>
<rect key="frame" x="363" y="15" width="50" height="50"/>
<constraints>
<constraint firstAttribute="width" constant="71" id="ACP-i0-gK8"/>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="71" id="DS1-a1-ep3"/>
<constraint firstAttribute="height" constant="71" id="ltx-rR-am0"/>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="71" id="s7u-Zs-z2Q"/>
<constraint firstAttribute="width" secondItem="AoF-Up-Yu5" secondAttribute="height" multiplier="1:1" id="6LJ-QR-WEY"/>
</constraints>
<state key="normal" image="hangUpButton"/>
<state key="normal" image="end_call"/>
<connections>
<action selector="hangUp:" destination="t2c-G5-7AE" eventType="touchUpInside" id="SUH-Gd-OXj"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="7vf-kV-eQ5" userLabel="videoMuteButton">
<rect key="frame" x="283" y="20.5" width="39" height="39"/>
</subviews>
<color key="backgroundColor" red="0.18431372549019609" green="0.1764705882352941" blue="0.16470588235294117" alpha="0.5" colorSpace="custom" customColorSpace="displayP3"/>
<constraints>
<constraint firstItem="r9w-8l-Zxj" firstAttribute="top" secondItem="sK0-tB-H9u" secondAttribute="top" constant="15" id="00C-1i-sGc"/>
<constraint firstItem="AoF-Up-Yu5" firstAttribute="bottom" secondItem="r9w-8l-Zxj" secondAttribute="bottom" id="1kB-OK-b0L"/>
<constraint firstItem="r9w-8l-Zxj" firstAttribute="leading" secondItem="sK0-tB-H9u" secondAttribute="leading" priority="900" constant="5" id="6B7-94-Myo"/>
<constraint firstAttribute="trailing" secondItem="AoF-Up-Yu5" secondAttribute="trailing" priority="900" constant="5" id="DgE-WQ-KQm"/>
<constraint firstItem="r9w-8l-Zxj" firstAttribute="top" secondItem="sK0-tB-H9u" secondAttribute="top" priority="900" constant="5" id="Ft5-cy-2d0"/>
<constraint firstAttribute="bottom" secondItem="r9w-8l-Zxj" secondAttribute="bottom" constant="30" id="G9u-kw-mVz"/>
<constraint firstItem="AoF-Up-Yu5" firstAttribute="top" secondItem="r9w-8l-Zxj" secondAttribute="top" id="H4s-Y8-HAI"/>
<constraint firstAttribute="trailing" secondItem="AoF-Up-Yu5" secondAttribute="trailing" constant="15" id="flB-AR-zo3"/>
<constraint firstAttribute="bottom" secondItem="r9w-8l-Zxj" secondAttribute="bottom" priority="900" constant="5" id="n0r-vM-wKh"/>
<constraint firstItem="r9w-8l-Zxj" firstAttribute="leading" secondItem="sK0-tB-H9u" secondAttribute="leading" constant="15" id="x13-gZ-cYp"/>
</constraints>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="2nj-Va-L7c" userLabel="TopBar">
<rect key="frame" x="0.0" y="0.0" width="428" height="100"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="n8H-om-4LV">
<rect key="frame" x="398" y="75" width="25" height="25"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="height" constant="39" id="7Rd-r8-zJP"/>
<constraint firstAttribute="width" constant="39" id="mwK-hd-jIy"/>
<constraint firstAttribute="height" constant="25" id="dyc-7x-bTk"/>
<constraint firstAttribute="width" constant="25" id="tYx-Ul-0Pf"/>
</constraints>
<state key="normal" title="Button" image="videoMuteButton"/>
<state key="selected" image="videoMuteButtonSelected"/>
<fontDescription key="fontDescription" type="system" pointSize="12"/>
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<state key="normal" title="●">
<color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</state>
<connections>
<action selector="didClickVideoMuteButton:" destination="t2c-G5-7AE" eventType="touchUpInside" id="Hza-Ft-NNB"/>
<action selector="circleFloatBtnTapped:" destination="t2c-G5-7AE" eventType="touchUpInside" id="2mC-ry-Lis"/>
</connections>
</button>
<view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="9g2-IH-l8e" userLabel="controlButtonSpacer">
<rect key="frame" x="396" y="0.0" width="18" height="80"/>
<constraints>
<constraint firstAttribute="width" priority="750" constant="18" id="7f6-zT-RoW"/>
<constraint firstAttribute="height" constant="80" id="cmy-aX-hM0"/>
</constraints>
</view>
<view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="a4N-1U-1Yt" userLabel="controlButtonSpacer">
<rect key="frame" x="328" y="0.0" width="18" height="80"/>
<constraints>
<constraint firstAttribute="width" priority="750" constant="18" id="X8j-wu-KrQ"/>
<constraint firstAttribute="height" constant="80" id="vkd-63-bmN"/>
</constraints>
</view>
<view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="GbA-j6-191" userLabel="controlButtonSpacer">
<rect key="frame" x="259" y="0.0" width="18" height="80"/>
<constraints>
<constraint firstAttribute="width" priority="750" constant="18" id="Bz0-tD-Q4d"/>
<constraint firstAttribute="height" constant="80" id="Ovg-ap-S7E"/>
</constraints>
</view>
<view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="h0g-z5-Y95" userLabel="controlButtonSpacer">
<rect key="frame" x="143" y="0.0" width="18" height="80"/>
<constraints>
<constraint firstAttribute="height" constant="80" id="Vaj-Va-jZn"/>
<constraint firstAttribute="width" priority="750" constant="18" id="Wv5-ys-Ho4"/>
</constraints>
</view>
<view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="gop-Yl-2fm" userLabel="controlButtonSpacer">
<rect key="frame" x="73" y="0.0" width="18" height="80"/>
<constraints>
<constraint firstAttribute="width" priority="750" constant="18" id="iqX-Wz-fb1"/>
<constraint firstAttribute="height" constant="80" id="j0P-xC-zSP"/>
</constraints>
</view>
<view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="JCG-W9-5ko" userLabel="controlButtonSpacer">
<rect key="frame" x="6" y="0.0" width="18" height="80"/>
<constraints>
<constraint firstAttribute="width" priority="750" constant="18" id="La6-ZY-Qs8"/>
<constraint firstAttribute="height" constant="80" id="mf9-Qg-gVZ"/>
</constraints>
</view>
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="10" translatesAutoresizingMaskIntoConstraints="NO" id="bvC-Fa-s3m">
<rect key="frame" x="15" y="50" width="398" height="30"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Al-Anoud Al-Anoud" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Zct-62-YE6">
<rect key="frame" x="0.0" y="0.0" width="328" height="30"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="25"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="8Xn-w6-Mox">
<rect key="frame" x="338" y="3" width="60" height="24"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="-- : --" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Btq-Ll-BPH">
<rect key="frame" x="13.333333333333314" y="5.0000000000000009" width="33.666666666666664" height="14.333333333333336"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="12"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" red="0.0018553551427920477" green="0.59902576721627221" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
<constraints>
<constraint firstAttribute="width" constant="60" id="InL-xK-60G"/>
<constraint firstItem="Btq-Ll-BPH" firstAttribute="centerY" secondItem="8Xn-w6-Mox" secondAttribute="centerY" id="Vbi-zv-vUQ"/>
<constraint firstItem="Btq-Ll-BPH" firstAttribute="centerX" secondItem="8Xn-w6-Mox" secondAttribute="centerX" id="WNT-XA-AW6"/>
<constraint firstAttribute="height" constant="24" id="Zck-PD-EN0"/>
</constraints>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius">
<integer key="value" value="12"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</view>
</subviews>
</stackView>
</subviews>
<color key="backgroundColor" red="0.18431372549019609" green="0.1764705882352941" blue="0.16470588235294117" alpha="0.80000000000000004" colorSpace="custom" customColorSpace="displayP3"/>
<constraints>
<constraint firstItem="9g2-IH-l8e" firstAttribute="leading" secondItem="ygd-al-G4F" secondAttribute="trailing" constant="5" id="0Bv-9b-HQh"/>
<constraint firstItem="a4N-1U-1Yt" firstAttribute="leading" secondItem="7vf-kV-eQ5" secondAttribute="trailing" constant="6" id="4vp-mw-gRE"/>
<constraint firstAttribute="height" constant="80" id="5QG-54-ulG"/>
<constraint firstItem="GbA-j6-191" firstAttribute="width" secondItem="h0g-z5-Y95" secondAttribute="width" id="5cw-b2-SPx"/>
<constraint firstItem="h0g-z5-Y95" firstAttribute="width" secondItem="gop-Yl-2fm" secondAttribute="width" id="7oE-3V-2Gc"/>
<constraint firstItem="7vf-kV-eQ5" firstAttribute="centerY" secondItem="sK0-tB-H9u" secondAttribute="centerY" id="8zH-6k-6Wz"/>
<constraint firstItem="gop-Yl-2fm" firstAttribute="leading" secondItem="NUO-2p-pzL" secondAttribute="trailing" constant="5" id="Bbh-OY-I9f"/>
<constraint firstItem="NUO-2p-pzL" firstAttribute="leading" secondItem="JCG-W9-5ko" secondAttribute="trailing" constant="5" id="Ccz-Qq-RXf"/>
<constraint firstItem="gop-Yl-2fm" firstAttribute="centerY" secondItem="sK0-tB-H9u" secondAttribute="centerY" id="DE6-nZ-rEI"/>
<constraint firstItem="GbA-j6-191" firstAttribute="centerY" secondItem="sK0-tB-H9u" secondAttribute="centerY" id="EVH-xy-wpT"/>
<constraint firstItem="ygd-al-G4F" firstAttribute="leading" secondItem="a4N-1U-1Yt" secondAttribute="trailing" constant="6" id="FNy-Br-FIj"/>
<constraint firstItem="NUO-2p-pzL" firstAttribute="centerY" secondItem="sK0-tB-H9u" secondAttribute="centerY" id="Fme-oB-F7i"/>
<constraint firstItem="GbA-j6-191" firstAttribute="leading" secondItem="AoF-Up-Yu5" secondAttribute="trailing" constant="14" id="GxO-Fe-iqR"/>
<constraint firstItem="JCG-W9-5ko" firstAttribute="centerY" secondItem="sK0-tB-H9u" secondAttribute="centerY" id="HPP-q4-IDT"/>
<constraint firstItem="h0g-z5-Y95" firstAttribute="leading" secondItem="N2h-FX-kF5" secondAttribute="trailing" constant="6" id="I2J-pE-7yx"/>
<constraint firstItem="7vf-kV-eQ5" firstAttribute="leading" secondItem="GbA-j6-191" secondAttribute="trailing" constant="6" id="MVo-If-UVQ"/>
<constraint firstItem="a4N-1U-1Yt" firstAttribute="width" secondItem="GbA-j6-191" secondAttribute="width" id="Me9-IC-YE2"/>
<constraint firstItem="N2h-FX-kF5" firstAttribute="leading" secondItem="gop-Yl-2fm" secondAttribute="trailing" constant="7" id="RHr-s8-sAg"/>
<constraint firstItem="h0g-z5-Y95" firstAttribute="centerY" secondItem="sK0-tB-H9u" secondAttribute="centerY" id="RJj-Uh-H2E"/>
<constraint firstItem="a4N-1U-1Yt" firstAttribute="centerY" secondItem="sK0-tB-H9u" secondAttribute="centerY" id="Tb2-6o-Wqh"/>
<constraint firstItem="AoF-Up-Yu5" firstAttribute="leading" secondItem="h0g-z5-Y95" secondAttribute="trailing" constant="13" id="VZS-xZ-nbe"/>
<constraint firstItem="NUO-2p-pzL" firstAttribute="leading" secondItem="JCG-W9-5ko" secondAttribute="trailing" constant="5" id="Z1j-5z-34E"/>
<constraint firstItem="7vf-kV-eQ5" firstAttribute="leading" secondItem="GbA-j6-191" secondAttribute="trailing" constant="6" id="ZjD-GK-SkY"/>
<constraint firstItem="ygd-al-G4F" firstAttribute="centerY" secondItem="sK0-tB-H9u" secondAttribute="centerY" id="bJA-GM-b2E"/>
<constraint firstItem="ygd-al-G4F" firstAttribute="leading" secondItem="a4N-1U-1Yt" secondAttribute="trailing" constant="6" id="eFd-3F-A5f"/>
<constraint firstItem="JCG-W9-5ko" firstAttribute="leading" secondItem="sK0-tB-H9u" secondAttribute="leading" constant="6" id="f0s-gb-347"/>
<constraint firstItem="gop-Yl-2fm" firstAttribute="leading" secondItem="NUO-2p-pzL" secondAttribute="trailing" constant="5" id="frf-I5-Rs9"/>
<constraint firstItem="gop-Yl-2fm" firstAttribute="width" secondItem="JCG-W9-5ko" secondAttribute="width" id="i84-jd-Hwr"/>
<constraint firstItem="N2h-FX-kF5" firstAttribute="leading" secondItem="gop-Yl-2fm" secondAttribute="trailing" constant="7" id="jlv-lR-Nlf"/>
<constraint firstItem="N2h-FX-kF5" firstAttribute="centerY" secondItem="sK0-tB-H9u" secondAttribute="centerY" id="nU8-Hw-s0U"/>
<constraint firstItem="9g2-IH-l8e" firstAttribute="centerY" secondItem="sK0-tB-H9u" secondAttribute="centerY" id="pxq-U1-zP5"/>
<constraint firstItem="AoF-Up-Yu5" firstAttribute="centerY" secondItem="sK0-tB-H9u" secondAttribute="centerY" id="ql2-fm-OeZ"/>
<constraint firstItem="9g2-IH-l8e" firstAttribute="width" secondItem="a4N-1U-1Yt" secondAttribute="width" id="uXs-zM-gyf"/>
<constraint firstItem="h0g-z5-Y95" firstAttribute="leading" secondItem="N2h-FX-kF5" secondAttribute="trailing" constant="6" id="uia-9X-UrW"/>
<constraint firstItem="a4N-1U-1Yt" firstAttribute="leading" secondItem="7vf-kV-eQ5" secondAttribute="trailing" constant="6" id="vgH-Oj-HJc"/>
<constraint firstAttribute="trailing" secondItem="9g2-IH-l8e" secondAttribute="trailing" id="vng-6I-eYj"/>
<constraint firstAttribute="height" constant="100" id="6eP-Zl-97q"/>
<constraint firstAttribute="trailing" secondItem="n8H-om-4LV" secondAttribute="trailing" constant="5" id="7Uq-4Q-ywD"/>
<constraint firstAttribute="bottom" secondItem="bvC-Fa-s3m" secondAttribute="bottom" priority="900" constant="5" id="HgY-Dw-B9R"/>
<constraint firstAttribute="bottom" secondItem="n8H-om-4LV" secondAttribute="bottom" id="Nr2-zL-avJ"/>
<constraint firstAttribute="bottom" secondItem="bvC-Fa-s3m" secondAttribute="bottom" constant="20" id="PWn-Sb-O8B"/>
<constraint firstItem="bvC-Fa-s3m" firstAttribute="leading" secondItem="2nj-Va-L7c" secondAttribute="leading" constant="15" id="YH4-fV-fkQ"/>
<constraint firstAttribute="trailing" secondItem="bvC-Fa-s3m" secondAttribute="trailing" constant="15" id="oq6-Oi-i7g"/>
<constraint firstAttribute="height" priority="900" constant="25" id="s8H-wK-WPB"/>
</constraints>
</view>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="videoMutedIndicator" translatesAutoresizingMaskIntoConstraints="NO" id="FEH-bC-Hnh" userLabel="localVideoMutedIndicator">
<rect key="frame" x="276" y="235" width="36" height="30"/>
<constraints>
<constraint firstAttribute="height" constant="30" id="1iv-w5-GDT"/>
<constraint firstAttribute="width" constant="36" id="AwX-vQ-Ynn"/>
</constraints>
</imageView>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="VIH-1T-Ife" firstAttribute="top" secondItem="sK0-tB-H9u" secondAttribute="bottom" constant="42" id="1tT-KH-NEk"/>
<constraint firstAttribute="trailing" secondItem="sK0-tB-H9u" secondAttribute="trailing" id="6K9-n9-ceJ"/>
<constraint firstAttribute="bottom" secondItem="6C5-o5-R1n" secondAttribute="bottom" constant="22" id="76x-6M-xOb"/>
<constraint firstItem="yS9-HT-vp8" firstAttribute="top" secondItem="sK0-tB-H9u" secondAttribute="bottom" constant="24" id="9zS-Fw-oaT"/>
<constraint firstItem="eOT-Jr-GUl" firstAttribute="centerX" secondItem="PoR-7r-yNe" secondAttribute="centerX" id="HMH-Hc-ofZ"/>
<constraint firstItem="FEH-bC-Hnh" firstAttribute="centerY" secondItem="0ZP-s2-kM5" secondAttribute="centerY" id="K8x-bd-bS9"/>
<constraint firstItem="eOT-Jr-GUl" firstAttribute="width" secondItem="PoR-7r-yNe" secondAttribute="width" id="PH6-qw-V6p"/>
<constraint firstItem="eOT-Jr-GUl" firstAttribute="centerY" secondItem="PoR-7r-yNe" secondAttribute="centerY" id="Prz-4m-LHB"/>
<constraint firstItem="0ZP-s2-kM5" firstAttribute="width" secondItem="PoR-7r-yNe" secondAttribute="width" multiplier="0.25" id="QCm-L0-oF7"/>
<constraint firstItem="sK0-tB-H9u" firstAttribute="leading" secondItem="PoR-7r-yNe" secondAttribute="leading" id="TPj-1d-lx6"/>
<constraint firstItem="eOT-Jr-GUl" firstAttribute="leading" secondItem="PoR-7r-yNe" secondAttribute="leading" id="4xz-lj-CXd"/>
<constraint firstAttribute="bottom" secondItem="eOT-Jr-GUl" secondAttribute="bottom" id="5Ae-F1-hSz"/>
<constraint firstItem="WPH-QE-Fvp" firstAttribute="leading" secondItem="PoR-7r-yNe" secondAttribute="leading" id="5Yr-p4-csA"/>
<constraint firstItem="2nj-Va-L7c" firstAttribute="leading" secondItem="PoR-7r-yNe" secondAttribute="leading" id="8Gx-Tb-bpo"/>
<constraint firstItem="O5s-CJ-RFv" firstAttribute="leading" secondItem="PoR-7r-yNe" secondAttribute="leading" id="Hyn-9r-k8s"/>
<constraint firstItem="sK0-tB-H9u" firstAttribute="leading" secondItem="PoR-7r-yNe" secondAttribute="leading" id="Itp-Pe-kh9"/>
<constraint firstItem="2nj-Va-L7c" firstAttribute="top" secondItem="PoR-7r-yNe" secondAttribute="top" id="LDC-Rq-79f"/>
<constraint firstItem="XHG-Db-3Px" firstAttribute="height" secondItem="eOT-Jr-GUl" secondAttribute="height" multiplier="0.196544" id="LwG-7m-Ihd"/>
<constraint firstAttribute="trailing" secondItem="eOT-Jr-GUl" secondAttribute="trailing" id="Pcn-K9-H92"/>
<constraint firstAttribute="bottom" secondItem="sK0-tB-H9u" secondAttribute="bottom" id="Tu5-Op-mnF"/>
<constraint firstItem="CL1-2v-m1L" firstAttribute="centerY" secondItem="PoR-7r-yNe" secondAttribute="centerY" id="UGv-46-ri8"/>
<constraint firstAttribute="trailingMargin" secondItem="0ZP-s2-kM5" secondAttribute="trailing" constant="20" id="UZX-Yj-Efk"/>
<constraint firstItem="A0G-cT-yji" firstAttribute="width" secondItem="0ZP-s2-kM5" secondAttribute="width" constant="55.5" id="WMH-vd-rQe"/>
<constraint firstAttribute="trailing" secondItem="O5s-CJ-RFv" secondAttribute="trailing" id="VcP-8e-3Un"/>
<constraint firstItem="CL1-2v-m1L" firstAttribute="centerX" secondItem="PoR-7r-yNe" secondAttribute="centerX" id="Xt8-k3-fDj"/>
<constraint firstItem="FEH-bC-Hnh" firstAttribute="centerX" secondItem="0ZP-s2-kM5" secondAttribute="centerX" constant="-28.25" id="Yvq-7p-bjS"/>
<constraint firstAttribute="bottom" secondItem="yS9-HT-vp8" secondAttribute="bottom" constant="22" id="bVF-wg-lH8"/>
<constraint firstItem="6C5-o5-R1n" firstAttribute="leading" secondItem="PoR-7r-yNe" secondAttribute="leading" constant="14" id="der-54-SZ4"/>
<constraint firstItem="A0G-cT-yji" firstAttribute="centerX" secondItem="0ZP-s2-kM5" secondAttribute="centerX" constant="-27.75" id="ga5-7D-8CU"/>
<constraint firstItem="A0G-cT-yji" firstAttribute="height" secondItem="0ZP-s2-kM5" secondAttribute="height" id="hMV-RD-NwE"/>
<constraint firstItem="A0G-cT-yji" firstAttribute="centerY" secondItem="0ZP-s2-kM5" secondAttribute="centerY" id="nBC-L0-5Pe"/>
<constraint firstItem="0ZP-s2-kM5" firstAttribute="height" secondItem="PoR-7r-yNe" secondAttribute="height" multiplier="0.25" id="pq3-Iv-N4l"/>
<constraint firstItem="0ZP-s2-kM5" firstAttribute="top" secondItem="BIa-yr-ZMY" secondAttribute="bottom" constant="50" id="qrs-H2-Rw4"/>
<constraint firstItem="eOT-Jr-GUl" firstAttribute="height" secondItem="PoR-7r-yNe" secondAttribute="height" id="rbk-Lc-qip"/>
<constraint firstAttribute="trailing" secondItem="yS9-HT-vp8" secondAttribute="trailing" constant="9" id="vEv-GY-lDH"/>
<constraint firstItem="6C5-o5-R1n" firstAttribute="top" secondItem="sK0-tB-H9u" secondAttribute="bottom" constant="24" id="xhd-lu-lFH"/>
<constraint firstItem="CL1-2v-m1L" firstAttribute="width" secondItem="PoR-7r-yNe" secondAttribute="width" multiplier="0.25" id="Yce-bz-JBd"/>
<constraint firstItem="WPH-QE-Fvp" firstAttribute="top" secondItem="PoR-7r-yNe" secondAttribute="top" id="ddA-LD-2O3"/>
<constraint firstItem="sK0-tB-H9u" firstAttribute="top" secondItem="O5s-CJ-RFv" secondAttribute="bottom" id="eOi-4p-iMm"/>
<constraint firstAttribute="trailing" secondItem="WPH-QE-Fvp" secondAttribute="trailing" id="evw-Oc-GgJ"/>
<constraint firstItem="O5s-CJ-RFv" firstAttribute="top" secondItem="2nj-Va-L7c" secondAttribute="bottom" id="fLH-dK-xpa"/>
<constraint firstItem="eOT-Jr-GUl" firstAttribute="top" secondItem="PoR-7r-yNe" secondAttribute="top" id="gFm-X8-RIU"/>
<constraint firstAttribute="trailing" secondItem="sK0-tB-H9u" secondAttribute="trailing" id="gSh-7K-3p5"/>
<constraint firstAttribute="bottom" secondItem="WPH-QE-Fvp" secondAttribute="bottom" id="sYs-wp-cot"/>
<constraint firstAttribute="trailing" secondItem="2nj-Va-L7c" secondAttribute="trailing" id="vQp-Bv-IVU"/>
</constraints>
</view>
<navigationItem key="navigationItem" id="s5b-6P-Hwj"/>
<connections>
<outlet property="btnMinimize" destination="UZ6-EY-a59" id="bDC-LS-qlX"/>
<outlet property="btnScreenTap" destination="WPH-QE-Fvp" id="wiN-5a-WJO"/>
<outlet property="camSwitchBtn" destination="lar-xK-rhR" id="tde-s5-Mji"/>
<outlet property="controlButtons" destination="sK0-tB-H9u" id="mDK-cq-oQz"/>
<outlet property="localVideo" destination="0ZP-s2-kM5" id="C68-k7-h5j"/>
<outlet property="localVideoMutedBg" destination="A0G-cT-yji" id="5Ia-9g-7CT"/>
<outlet property="localVideoMutedIndicator" destination="FEH-bC-Hnh" id="PKd-gQ-Jct"/>
<outlet property="remoteVideo" destination="eOT-Jr-GUl" id="VKE-el-YGR"/>
<outlet property="fullVideoView" destination="eOT-Jr-GUl" id="VKE-el-YGR"/>
<outlet property="hideVideoBtn" destination="n8H-om-4LV" id="tTp-Ep-XKS"/>
<outlet property="lblCallDuration" destination="Btq-Ll-BPH" id="Jdh-yK-PEx"/>
<outlet property="lblRemoteUsername" destination="Zct-62-YE6" id="OQ0-mf-mvc"/>
<outlet property="localVideoContainer" destination="O5s-CJ-RFv" id="bh9-Yr-UGv"/>
<outlet property="localVideoMutedBg" destination="6y1-an-W7F" id="5FF-Ak-Vxb"/>
<outlet property="micMuteBtn" destination="N2h-FX-kF5" id="zgK-g1-Ndr"/>
<outlet property="remoteVideoMutedIndicator" destination="CL1-2v-m1L" id="aJ0-QC-m9Y"/>
<outlet property="smallVideoView" destination="0ZP-s2-kM5" id="C68-k7-h5j"/>
<outlet property="topBar" destination="2nj-Va-L7c" id="Zt4-J2-uw8"/>
<outlet property="videoMuteBtn" destination="NUO-2p-pzL" id="XCc-nu-N8P"/>
<outletCollection property="minimizeConstraint" destination="s8H-wK-WPB" collectionClass="NSMutableArray" id="XTM-jR-Cos"/>
<outletCollection property="minimizeConstraint" destination="HgY-Dw-B9R" collectionClass="NSMutableArray" id="Fi8-YX-9iQ"/>
<outletCollection property="minimizeConstraint" destination="Ft5-cy-2d0" collectionClass="NSMutableArray" id="Ze3-H8-gj4"/>
<outletCollection property="minimizeConstraint" destination="n0r-vM-wKh" collectionClass="NSMutableArray" id="IaQ-TS-ng8"/>
<outletCollection property="maximisedConstraint" destination="G9u-kw-mVz" collectionClass="NSMutableArray" id="TaK-5m-byK"/>
<outletCollection property="maximisedConstraint" destination="00C-1i-sGc" collectionClass="NSMutableArray" id="vmx-rp-JZI"/>
<outletCollection property="maximisedConstraint" destination="PWn-Sb-O8B" collectionClass="NSMutableArray" id="pK4-yA-36p"/>
<outletCollection property="maximisedConstraint" destination="6eP-Zl-97q" collectionClass="NSMutableArray" id="dxk-ol-uAW"/>
<outletCollection property="maximisedConstraint" destination="3L0-3h-Bxg" collectionClass="NSMutableArray" id="w3Q-AR-TQY"/>
<outletCollection property="minimizeConstraint" destination="y90-hA-p5Z" collectionClass="NSMutableArray" id="INL-Bc-eTM"/>
<outletCollection property="minimizeConstraint" destination="DgE-WQ-KQm" collectionClass="NSMutableArray" id="zy0-aI-bnY"/>
<outletCollection property="maximisedConstraint" destination="flB-AR-zo3" collectionClass="NSMutableArray" id="hwu-U4-5s5"/>
<outletCollection property="minimizeConstraint" destination="6B7-94-Myo" collectionClass="NSMutableArray" id="epM-Kg-2yi"/>
<outletCollection property="maximisedConstraint" destination="x13-gZ-cYp" collectionClass="NSMutableArray" id="uLI-UQ-yIo"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="x33-EE-6Ak" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1685.5072463768117" y="1435.7142857142856"/>
</scene>
<!--Navigation Controller-->
<scene sceneID="GvA-TT-FAo">
<objects>
<navigationController restorationIdentifier="ViewControllerNav" storyboardIdentifier="ViewControllerNav" id="0WJ-RG-MEh" sceneMemberID="viewController">
<navigationBar key="navigationBar" contentMode="scaleToFill" id="S7n-df-cyC">
<rect key="frame" x="0.0" y="44" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<connections>
<segue destination="t2c-G5-7AE" kind="relationship" relationship="rootViewController" id="L74-If-nYJ"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="vSx-dK-kQe" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="880" y="1436"/>
<point key="canvasLocation" x="-797.66355140186909" y="2238.6609071274302"/>
</scene>
</scenes>
<resources>
<image name="hangUpButton" width="71" height="71"/>
<image name="localVideoMutedBg" width="90" height="120"/>
<image name="muteButton" width="39" height="39"/>
<image name="muteButtonSelected" width="39" height="39"/>
<image name="speakerOff" width="39" height="39"/>
<image name="speakerOffSelected" width="39" height="39"/>
<image name="switchCameraButton" width="39" height="39"/>
<image name="switchCameraButtonSelected" width="39" height="39"/>
<image name="videoMuteButton" width="39" height="39"/>
<image name="videoMuteButtonSelected" width="39" height="39"/>
<image name="camera_back" width="198" height="202"/>
<image name="camera_front" width="198" height="202"/>
<image name="end_call" width="488" height="488"/>
<image name="expand_video" width="202" height="199"/>
<image name="float_video" width="202" height="202"/>
<image name="mic_mute" width="198" height="199"/>
<image name="mic_unmute" width="198" height="202"/>
<image name="videoMutedIndicator" width="153.5" height="129"/>
<image name="video_mute" width="202" height="199"/>
<image name="video_unmute" width="202" height="202"/>
</resources>
</document>

@ -0,0 +1,27 @@
//
// Extensions.swift
// Runner
//
// Created by Zohaib Iqbal Kambrani on 09/06/2021.
// Copyright © 2021 The Chromium Authors. All rights reserved.
//
import AADraggableView
extension AADraggableView{
func enable(_ enable:Bool){
isEnabled = enable
if enable == false{
gestureRecognizers?.forEach({ gest in
removeGestureRecognizer(gest)
})
}
}
}
extension UIView{
func hidden(_ hidden:Bool, rootView:UIView){
UIView.transition(with: rootView, duration: 0.5, options: .transitionCrossDissolve, animations: {
self.isHidden = hidden
})
}
}

@ -0,0 +1,190 @@
//
// MainAppViewController.swift
// Runner
//
// Created by Zohaib Iqbal Kambrani on 08/06/2021.
// Copyright © 2021 The Chromium Authors. All rights reserved.
//
import Foundation
import AADraggableView
class MainAppViewController: FlutterViewController{
var videoCallContainer:AADraggableView!
var videoCallViewController:VideoCallViewController!
var videoCallFlutterResult:FlutterResult?
var vdoCallViewFloatRectConstraint:[NSLayoutConstraint]!
var vdoCallViewFullConstraint:[NSLayoutConstraint]!
var vdoCallViewFloatCircleConstraint:[NSLayoutConstraint]!
override func viewDidLoad() {
super.viewDidLoad()
initFlutterBridge()
prepareVideoCallView()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
}
var videoCallChannel:FlutterMethodChannel?
private func initFlutterBridge(){
videoCallChannel = FlutterMethodChannel(name: "Dr.cloudSolution/videoCall", binaryMessenger: binaryMessenger)
videoCallChannel?.setMethodCallHandler({
(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
switch call.method {
case "openVideoCall":
self.startVideoCall(result: result, call: call)
default:
result(FlutterMethodNotImplemented)
}
})
}
}
// Video Call Functions
extension MainAppViewController : ICallProtocol{
func prepareVideoCallView(){
videoCallContainer = AADraggableView(frame: CGRect(x: 0, y: 0, width: view.bounds.width, height: view.bounds.height))
videoCallContainer.backgroundColor = UIColor.black
videoCallContainer.isHidden = true
videoCallContainer.clipsToBounds = true
videoCallContainer.padding = 20
videoCallContainer.reposition = .edgesOnly
videoCallContainer.isEnabled = false
view.addSubview(videoCallContainer)
setVideoViewConstrints()
NSLayoutConstraint.activate(vdoCallViewFullConstraint)
NSLayoutConstraint.deactivate(vdoCallViewFloatRectConstraint)
ViewEmbedder.embed(
withIdentifier: "videoCall", // Storyboard ID
parent: self,
container: self.videoCallContainer){ vc in
self.videoCallViewController = vc as? VideoCallViewController
}
}
private func showVideo(show:Bool){
self.videoCallContainer.hidden(!show, rootView: view)
}
private func startVideoCall(result: @escaping FlutterResult, call:FlutterMethodCall) {
videoCallFlutterResult = result
if let arguments = call.arguments as? NSDictionary{
showVideoCallView(true)
videoCallViewController.onRectFloat = { isRectFloat in
self.rectFloatVideoCallView(isRectFloat)
}
videoCallViewController.onCircleFloat = { isCircleFloat in
self.circleFloatVideoCallView(isCircleFloat)
}
videoCallViewController.onCallConnect = {
self.videoCallChannel?.invokeMethod("onCallConnected",arguments: nil)
}
videoCallViewController.onCallDisconnect = {
self.showVideoCallView(false)
self.videoCallViewController.minimizeVideoState(state: false)
self.videoCallChannel?.invokeMethod("onCallDisconnected",arguments: nil)
}
videoCallViewController.callBack = self
videoCallViewController.start(params: VideoCallRequestParameters(dictionary: arguments))
}
}
private func rectFloatVideoCallView(_ value:Bool){
videoCallContainer.enable(value)
UIView.animate(withDuration: 0.5) {
if(value){
NSLayoutConstraint.deactivate(self.vdoCallViewFullConstraint)
NSLayoutConstraint.deactivate(self.vdoCallViewFloatCircleConstraint)
NSLayoutConstraint.activate(self.vdoCallViewFloatRectConstraint)
}else{
NSLayoutConstraint.deactivate(self.vdoCallViewFloatRectConstraint)
NSLayoutConstraint.deactivate(self.vdoCallViewFloatCircleConstraint)
NSLayoutConstraint.activate(self.vdoCallViewFullConstraint)
}
self.videoCallContainer.layer.cornerRadius = value ? 10 : 0
self.videoCallContainer.layer.borderColor = value ? UIColor.white.cgColor : nil
self.videoCallContainer.layer.borderWidth = value ? 2 : 0
self.view.layoutIfNeeded()
}
}
private func circleFloatVideoCallView(_ value:Bool){
videoCallContainer.reposition = value ? .sticky : .edgesOnly
UIView.animate(withDuration: 0.5) {
if(value){
NSLayoutConstraint.deactivate(self.vdoCallViewFullConstraint)
NSLayoutConstraint.deactivate(self.vdoCallViewFloatRectConstraint)
NSLayoutConstraint.activate(self.vdoCallViewFloatCircleConstraint)
self.videoCallContainer.layer.cornerRadius = 35
}else{
NSLayoutConstraint.activate(self.vdoCallViewFloatRectConstraint)
NSLayoutConstraint.deactivate(self.vdoCallViewFullConstraint)
NSLayoutConstraint.deactivate(self.vdoCallViewFloatCircleConstraint)
self.videoCallContainer.layer.cornerRadius = 10
}
self.videoCallContainer.layer.borderColor = value ? UIColor.white.cgColor : nil
self.videoCallContainer.layer.borderWidth = value ? 2 : 0
self.view.layoutIfNeeded()
}
}
private func showVideoCallView(_ value:Bool){
self.videoCallContainer.hidden(!value, rootView: view)
}
func sessionDone(res: Any) {
videoCallFlutterResult?(res)
}
func sessionNotResponded(res: Any) {
videoCallFlutterResult?(res)
}
func setVideoViewConstrints(){
videoCallContainer.layer.shadowColor = UIColor.black.cgColor
videoCallContainer.layer.shadowOffset = CGSize(width: 1, height: 1)
videoCallContainer.layer.shadowRadius = 5
let screen = UIScreen.main.bounds
videoCallContainer.translatesAutoresizingMaskIntoConstraints = false
vdoCallViewFullConstraint = [
videoCallContainer.topAnchor.constraint(equalTo: view.topAnchor),
videoCallContainer.leadingAnchor.constraint(equalTo: view.leadingAnchor),
videoCallContainer.widthAnchor.constraint(equalToConstant: screen.width),
videoCallContainer.heightAnchor.constraint(equalToConstant: screen.height)
]
vdoCallViewFloatRectConstraint = [
videoCallContainer.topAnchor.constraint(equalTo: view.topAnchor, constant: 40),
videoCallContainer.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
videoCallContainer.widthAnchor.constraint(equalToConstant: screen.width/3),
videoCallContainer.heightAnchor.constraint(equalToConstant: screen.height/3.5)
]
vdoCallViewFloatCircleConstraint = [
videoCallContainer.topAnchor.constraint(equalTo: view.topAnchor, constant: 40),
videoCallContainer.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
videoCallContainer.widthAnchor.constraint(equalToConstant: 70),
videoCallContainer.heightAnchor.constraint(equalToConstant: 70)
]
}
}

@ -0,0 +1,78 @@
//
// VCEmbeder.swift
// Runner
//
// Created by Zohaib Iqbal Kambrani on 08/06/2021.
// Copyright © 2021 The Chromium Authors. All rights reserved.
//
import Foundation
extension UIView {
func fill(to parent: UIView, animateDuration:Double = 0.5) {
self.topAnchor.constraint(equalTo: parent.topAnchor).isActive = true
self.leadingAnchor.constraint(equalTo: parent.leadingAnchor).isActive = true
self.bottomAnchor.constraint(equalTo: parent.bottomAnchor).isActive = true
self.trailingAnchor.constraint(equalTo: parent.trailingAnchor).isActive = true
UIView.animate(withDuration: animateDuration) {
parent.layoutIfNeeded()
}
}
func fillToParent(animateDuration:Double = 0.5) {
if let parent = self.superview{
self.topAnchor.constraint(equalTo: parent.topAnchor).isActive = true
self.leadingAnchor.constraint(equalTo: parent.leadingAnchor).isActive = true
self.bottomAnchor.constraint(equalTo: parent.bottomAnchor).isActive = true
self.trailingAnchor.constraint(equalTo: parent.trailingAnchor).isActive = true
UIView.animate(withDuration: animateDuration) {
parent.layoutIfNeeded()
}
}
}
func fillInTo(view:UIView) {
view.addSubview(self)
fillToParent()
}
}
class ViewEmbedder {
class func embed(
parent:UIViewController,
container:UIView,
child:UIViewController,
previous:UIViewController?){
if let previous = previous {
removeFromParent(vc: previous)
}
child.willMove(toParent: parent)
parent.addChild(child)
container.addSubview(child.view)
child.didMove(toParent: parent)
let w = container.frame.size.width;
let h = container.frame.size.height;
child.view.frame = CGRect(x: 0, y: 0, width: w, height: h)
child.view.backgroundColor = UIColor.black
child.view.fill(to: container)
}
class func removeFromParent(vc:UIViewController){
vc.willMove(toParent: nil)
vc.view.removeFromSuperview()
vc.removeFromParent()
}
class func embed(withIdentifier id:String, parent:UIViewController, container:UIView, completion:((UIViewController)->Void)? = nil){
let vc = parent.storyboard!.instantiateViewController(withIdentifier: id)
embed(
parent: parent,
container: container,
child: vc,
previous: parent.children.first
)
completion?(vc)
}
}

@ -0,0 +1,29 @@
import Foundation
class VideoCallRequestParameters{
var apiKey:String?
var sessionId:String?
var token:String?
var lang:String?
var vcId:Int?
var tokenId:String?
var generalId:String?
var doctorId:Int?
var baseUrl:String?
var patientName:String?
init(dictionary:NSDictionary){
self.apiKey = dictionary["kApiKey"] as? String
self.sessionId = dictionary["kSessionId"] as? String
self.token = dictionary["kToken"] as? String
self.lang = dictionary["appLang"] as? String
self.vcId = dictionary["VC_ID"] as? Int
self.tokenId = dictionary["TokenID"] as? String
self.generalId = dictionary["generalId"] as? String
self.doctorId = dictionary["DoctorId"] as? Int
self.baseUrl = dictionary["baseUrl"] as? String
self.patientName = dictionary["patientName"] as? String
}
}

@ -9,9 +9,9 @@
import UIKit
import OpenTok
import Alamofire
import AADraggableView
class ViewController: UIViewController {
class VideoCallViewController: UIViewController {
var session: OTSession?
var publisher: OTPublisher?
@ -34,18 +34,188 @@ class ViewController: UIViewController {
var seconds = 55
var isUserConnect : Bool = false
var onRectFloat:((Bool)->Void)? = nil
var onCircleFloat:((Bool)->Void)? = nil
var onCallConnect:(()->Void)? = nil
var onCallDisconnect:(()->Void)? = nil
@IBOutlet weak var lblRemoteUsername: UILabel!
// Bottom Actions
@IBOutlet weak var videoMuteBtn: UIButton!
@IBOutlet weak var micMuteBtn: UIButton!
@IBOutlet weak var camSwitchBtn: UIButton!
@IBOutlet var minimizeConstraint: [NSLayoutConstraint]!
@IBOutlet var maximisedConstraint: [NSLayoutConstraint]!
@IBOutlet weak var btnMinimize: UIButton!
@IBOutlet weak var hideVideoBtn: UIButton!
var localVideoDraggable:AADraggableView?
@IBOutlet weak var controlButtons: UIView!
@IBOutlet weak var remoteVideoMutedIndicator: UIImageView!
@IBOutlet weak var localVideoMutedBg: UIView!
@IBOutlet weak var btnScreenTap: UIButton!
@IBOutlet weak var localVideoContainer: UIView!
@IBOutlet weak var topBar: UIView!
@IBOutlet weak var lblCallDuration: UILabel!
@IBOutlet weak var fullVideoView: UIView!
@IBOutlet weak var smallVideoView: UIView!{
didSet{
smallVideoView.layer.borderColor = UIColor.white.cgColor
localVideoDraggable = smallVideoView?.superview as? AADraggableView
localVideoDraggable?.reposition = .edgesOnly
}
}
override func viewDidLoad() {
super.viewDidLoad()
super.viewDidLoad()
localVideoDraggable?.respectedView = localVideoContainer
}
@objc func click(gesture:UIGestureRecognizer){
gesture.view?.removeFromSuperview()
}
@IBAction func btnOnScreenTapped(_ sender: Any) {
if(hideVideoBtn.isSelected){
circleFloatBtnTapped(hideVideoBtn)
}else if(btnMinimize.isSelected){
btnMinimizeTapped(btnMinimize)
}
}
@IBAction func btnSwipeVideoTapped(_ sender: Any) {
// let smallVdoRender = smallVideoView.subviews.first
// let fullVdoRender = fullVideoView.subviews.first
// if let vdo = smallVdoRender{
// fullVideoView.addSubview(vdo)
// }
// if let vdo = fullVdoRender{
// smallVideoView.addSubview(vdo)
// }
//
// layoutVideoRenderViews()
}
@IBAction func didClickMuteButton(_ sender: UIButton) {
sender.isSelected = !sender.isSelected
publisher!.publishAudio = !sender.isSelected
}
@IBAction func didClickSpeakerButton(_ sender: UIButton) {
sender.isSelected = !sender.isSelected
subscriber?.subscribeToAudio = !sender.isSelected
}
@IBAction func didClickVideoMuteButton(_ sender: UIButton) {
sender.isSelected = !sender.isSelected
if publisher!.publishVideo {
publisher!.publishVideo = false
} else {
publisher!.publishVideo = true
}
smallVideoView.isHidden = sender.isSelected
localVideoMutedBg.isHidden = !sender.isSelected
}
@IBAction func didClickSwitchCameraButton(_ sender: UIButton) {
sender.isSelected = !sender.isSelected
if sender.isSelected {
publisher!.cameraPosition = AVCaptureDevice.Position.front
} else {
publisher!.cameraPosition = AVCaptureDevice.Position.back
}
}
@IBAction func hangUp(_ sender: UIButton) {
callBack?.sessionDone(res:["callResponse":"CallEnd"])
sessionDisconnect()
}
@IBAction func circleFloatBtnTapped(_ sender: UIButton) {
sender.isSelected = !sender.isSelected
onCircleFloat?(sender.isSelected)
topBar.isHidden = sender.isSelected
controlButtons.isHidden = sender.isSelected
smallVideoView.isHidden = sender.isSelected
self.publisher?.view?.layoutIfNeeded()
}
@IBAction func btnMinimizeTapped(_ sender: UIButton) {
minimizeVideoState(state: !sender.isSelected)
btnScreenTap.isHidden = !sender.isSelected
}
func minimizeVideoState(state:Bool){
btnMinimize.isSelected = state
onRectFloat?(state)
setupButtons()
askForMicrophonePermission()
requestCameraPermissionsIfNeeded()
hideVideoMuted()
setupSession()
}
NSLayoutConstraint.activate(state ? minimizeConstraint : maximisedConstraint)
NSLayoutConstraint.deactivate(state ? maximisedConstraint : minimizeConstraint)
localVideoDraggable?.enable(!state)
lblRemoteUsername.isHidden = state
hideVideoBtn.isHidden = !state
lblCallDuration.superview?.isHidden = !hideVideoBtn.isHidden
UIView.animate(withDuration: 0.5) {
self.videoMuteBtn.isHidden = state
self.micMuteBtn.isHidden = state
self.camSwitchBtn.isHidden = state
self.layoutVideoRenderViews()
}
}
func layoutVideoRenderViews(){
if let publisherVdoSize = publisher?.view?.superview?.bounds.size{
publisher?.view?.frame = CGRect(x: 0, y: 0, width: publisherVdoSize.width, height: publisherVdoSize.height)
}
if let subscriberVdoSize = subscriber?.view?.superview?.bounds.size{
subscriber?.view?.frame = CGRect(x: 0, y: 0, width: subscriberVdoSize.width, height: subscriberVdoSize.height)
}
}
var durationTimer:Timer?;
func startUpdateCallDuration(){
var seconds = 0
durationTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in
seconds = seconds+1
let durationSegments = (seconds / 3600, (seconds % 3600) / 60, (seconds % 3600) % 60)
let hours = String(format: "%02d", durationSegments.0)
let mins = String(format: "%02d", durationSegments.1)
let secs = String(format: "%02d", durationSegments.2)
let durationString = "\(mins):\(secs)"
self.lblCallDuration.text = durationString
}
}
func start(params:VideoCallRequestParameters){
lblRemoteUsername.text = params.patientName ?? "- - -"
btnScreenTap.isHidden = true
hideVideoBtn.isHidden = true
self.kApiKey = params.apiKey ?? ""
self.kSessionId = params.sessionId ?? ""
self.kToken = params.token ?? ""
self.VC_ID = params.vcId ?? 0
self.generalid = params.generalId ?? ""
self.TokenID = params.tokenId ?? ""
self.DoctorId = params.doctorId ?? 0
self.baseUrl = params.baseUrl ?? ""
askForMicrophonePermission()
requestCameraPermissionsIfNeeded()
hideVideoMuted()
setupSession()
}
private func changeCallStatus(callStatus:Int){
let URL_USER_REGISTER = baseUrl+"LiveCareApi/DoctorApp/ChangeCallStatus"
@ -107,13 +277,6 @@ class ViewController: UIViewController {
}
}
func setupButtons() {
perform(#selector(hideControlButtons), with: nil, afterDelay: 3)
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(remoteVideoTapped(_:)))
view.addGestureRecognizer(tapGestureRecognizer)
view.isUserInteractionEnabled = true
}
// MARK: -Microphone Camera and Permission Request
func askForMicrophonePermission() {
@ -137,59 +300,16 @@ class ViewController: UIViewController {
// display a useful message asking the user to grant permissions from within Settings > Privacy > Camera
}
@IBAction func didClickMuteButton(_ sender: UIButton) {
sender.isSelected = !sender.isSelected
publisher!.publishAudio = !sender.isSelected
}
@IBAction func didClickSpeakerButton(_ sender: UIButton) {
sender.isSelected = !sender.isSelected
subscriber?.subscribeToAudio = !sender.isSelected
// resetHideButtonsTimer()
}
@IBAction func didClickVideoMuteButton(_ sender: UIButton) {
sender.isSelected = !sender.isSelected
if publisher!.publishVideo {
publisher!.publishVideo = false
} else {
publisher!.publishVideo = true
}
localVideo.isHidden = sender.isSelected
localVideoMutedBg.isHidden = !sender.isSelected
localVideoMutedIndicator.isHidden = !sender.isSelected
// resetHideButtonsTimer()
}
@IBAction func didClickSwitchCameraButton(_ sender: UIButton) {
sender.isSelected = !sender.isSelected
if sender.isSelected {
publisher!.cameraPosition = AVCaptureDevice.Position.front
} else {
publisher!.cameraPosition = AVCaptureDevice.Position.back
}
/// resetHideButtonsTimer()
}
@IBAction func hangUp(_ sender: UIButton) {
callBack?.sessionDone(res:["callResponse":"CallEnd"])
sessionDisconnect()
}
func sessionDisconnect() {
changeCallStatus(callStatus: 16)
if (session != nil) {
print("disconnecting....")
session!.disconnect(nil)
dismiss(animated: true)
return
}
dismiss(animated: true)
onCallDisconnect?()
durationTimer?.invalidate()
}
func requestCameraPermissionsIfNeeded() {
@ -226,7 +346,6 @@ class ViewController: UIViewController {
func hideVideoMuted() {
remoteVideoMutedIndicator.isHidden = true
localVideoMutedBg.isHidden = true
localVideoMutedIndicator.isHidden = true
}
func setupSession() {
@ -267,19 +386,6 @@ class ViewController: UIViewController {
@IBOutlet weak var localVideo: UIView!
@IBOutlet weak var remoteVideo: UIView!
@IBOutlet weak var controlButtons: UIView!
@IBOutlet weak var remoteVideoMutedIndicator: UIImageView!
@IBOutlet weak var localVideoMutedBg: UIImageView!
@IBOutlet weak var localVideoMutedIndicator: UIImageView!
@objc func updateTimer(){
seconds -= 1 //This will decrement(count down)the seconds.
print(seconds)
@ -293,17 +399,11 @@ class ViewController: UIViewController {
}
extension ViewController: OTSessionDelegate {
extension VideoCallViewController: OTSessionDelegate {
func sessionDidConnect(_ session: OTSession) {
print("The client connected to the OpenTok session.")
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: (#selector(ViewController.updateTimer)), userInfo: nil, repeats: true)
print("The client connected to the OpenTok session.")
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: (#selector(VideoCallViewController.updateTimer)), userInfo: nil, repeats: true)
setupPublisher()
}
@ -318,10 +418,12 @@ extension ViewController: OTSessionDelegate {
if error != nil {
showAlert(error?.localizedDescription)
}
publisher?.view!.frame = CGRect(x: localVideo.bounds.origin.x, y: localVideo.bounds.origin.y, width: localVideo.bounds.size.width, height: localVideo.bounds.size.height)
localVideo.addSubview((publisher?.view)!)
publisher?.view?.tag = 11
publisher?.view?.layer.cornerRadius = 5
publisher?.view?.clipsToBounds = true
smallVideoView.addSubview((publisher?.view)!)
layoutVideoRenderViews()
}
func sessionDidDisconnect(_ session: OTSession) {
@ -346,27 +448,27 @@ extension ViewController: OTSessionDelegate {
func session(_ session: OTSession, streamCreated stream: OTStream) {
subscriber = OTSubscriber(stream: stream, delegate: self)
guard let subscriber = subscriber else {
return
}
subscriber = OTSubscriber(stream: stream, delegate: self)
guard let subscriber = subscriber else {
return
}
var error: OTError?
session.subscribe(subscriber, error: &error)
guard error == nil else {
print(error!)
return
}
var error: OTError?
session.subscribe(subscriber, error: &error)
guard error == nil else {
print(error!)
return
}
guard let subscriberView = subscriber.view else {
return
}
subscriberView.frame = UIScreen.main.bounds
view.insertSubview(subscriberView, at: 0)
guard let subscriberView = subscriber.view else {
return
}
subscriberView.tag = 22
fullVideoView.addSubview(subscriberView)
layoutVideoRenderViews()
// if nil == subscriber {
// setupSubscribe(stream)
// }
startUpdateCallDuration()
onCallConnect?()
}
func setupSubscribe(_ stream: OTStream?) {
@ -408,19 +510,13 @@ extension ViewController: OTSessionDelegate {
}
extension ViewController: OTPublisherDelegate {
extension VideoCallViewController: OTPublisherDelegate {
func publisher(_ publisher: OTPublisherKit, didFailWithError error: OTError) {
print("The publisher failed: \(error)")
}
@objc func remoteVideoTapped(_ recognizer: UITapGestureRecognizer?) {
if controlButtons.isHidden {
controlButtons.isHidden = false
perform(#selector(hideControlButtons), with: nil, afterDelay: 3)
}
}
}
extension ViewController: OTSubscriberDelegate {
extension VideoCallViewController: OTSubscriberDelegate {
public func subscriberDidConnect(toStream subscriber: OTSubscriberKit) {
print("The subscriber did connect to the stream.")
}
@ -428,8 +524,5 @@ extension ViewController: OTSubscriberDelegate {
public func subscriber(_ subscriber: OTSubscriberKit, didFailWithError error: OTError) {
print("The subscriber failed to connect to the stream.")
}
@objc func hideControlButtons() {
controlButtons.isHidden = true
}
}

@ -113,7 +113,7 @@ class BaseAppClient {
parsed['AndroidLink'], parsed['IOSLink']);
}
if (!parsed['IsAuthenticated']) {
if (parsed['IsAuthenticated'] != null && !parsed['IsAuthenticated']) {
if (body['OTP_SendType'] != null) {
onFailure(getError(parsed), statusCode);
} else if (!isAllowAny) {

@ -12,7 +12,7 @@ class PatientSearchRequestModel {
String mobileNo;
String identificationNo;
int nursingStationID;
int clinicID;
int clinicID=0;
PatientSearchRequestModel(
{this.doctorID = 0,
@ -63,6 +63,7 @@ class PatientSearchRequestModel {
data['IdentificationNo'] = this.identificationNo;
data['NursingStationID'] = this.nursingStationID;
data['ClinicID'] = this.clinicID;
data['ProjectID'] = 0;
return data;
}
}

@ -3,9 +3,15 @@ import 'package:flutter/material.dart';
class NavigationService {
final GlobalKey<NavigatorState> navigatorKey =
new GlobalKey<NavigatorState>();
Future<dynamic> navigateTo(String routeName) {
return navigatorKey.currentState.pushNamed(routeName);
Future<dynamic> navigateTo(String routeName,{Object arguments}) {
return navigatorKey.currentState.pushNamed(routeName,arguments: arguments);
}
Future<dynamic> pushReplacementNamed(String routeName,{Object arguments}) {
return navigatorKey.currentState.pushReplacementNamed(routeName,arguments: arguments);
}
Future<dynamic> pushNamedAndRemoveUntil(String routeName) {
return navigatorKey.currentState.pushNamedAndRemoveUntil(routeName,(asd)=>false);
}

@ -0,0 +1,89 @@
import 'package:doctor_app_flutter/config/config.dart';
import 'package:doctor_app_flutter/config/shared_pref_kay.dart';
import 'package:doctor_app_flutter/core/service/base/base_service.dart';
import 'package:doctor_app_flutter/core/service/patient/LiveCarePatientServices.dart';
import 'package:doctor_app_flutter/models/doctor/doctor_profile_model.dart';
import 'package:doctor_app_flutter/models/livecare/end_call_req.dart';
import 'package:doctor_app_flutter/models/livecare/session_status_model.dart';
import 'package:doctor_app_flutter/models/livecare/start_call_res.dart';
import 'package:doctor_app_flutter/models/patient/patiant_info_model.dart';
import 'package:doctor_app_flutter/util/VideoChannel.dart';
import 'package:doctor_app_flutter/util/dr_app_toast_msg.dart';
import 'package:doctor_app_flutter/widgets/shared/loader/gif_loader_dialog_utils.dart';
import 'package:flutter/cupertino.dart';
import '../../locator.dart';
import '../../routes.dart';
import 'NavigationService.dart';
class VideoCallService extends BaseService{
StartCallRes startCallRes;
PatiantInformtion patient;
LiveCarePatientServices _liveCarePatientServices = locator<LiveCarePatientServices>();
openVideo(StartCallRes startModel,PatiantInformtion patientModel,VoidCallback onCallConnected, VoidCallback onCallDisconnected)async{
this.startCallRes = startModel;
this.patient = patientModel;
DoctorProfileModel doctorProfile = await getDoctorProfile(isGetProfile: true);
await VideoChannel.openVideoCallScreen(
kToken: startCallRes.openTokenID,//"T1==cGFydG5lcl9pZD00NzI0Nzk1NCZzaWc9NGIyZDljOTY3YjFiNWU1YzUzNzFmMjIyNjJmNmEzY2Y5NzZjOTdlYzpzZXNzaW9uX2lkPTFfTVg0ME56STBOemsxTkg1LU1UWXlNekEyTlRRMU9EVXhObjVrVFRoMFlVdFJXaXRYTWpadFZGZHFhSGxZVGpOdE1UVi1mZyZjcmVhdGVfdGltZT0xNjIzMDY1NDk1Jm5vbmNlPTAuMjM2Mjk0NTIwMTkyOTA4OTcmcm9sZT1wdWJsaXNoZXImZXhwaXJlX3RpbWU9MTYyNTY1NzQ5NCZpbml0aWFsX2xheW91dF9jbGFzc19saXN0PQ==",
kSessionId:startCallRes.openSessionID,//1_MX40NzI0Nzk1NH5-MTYyMzA2NTQ1ODUxNn5kTTh0YUtRWitXMjZtVFdqaHlYTjNtMTV-fg
kApiKey: '46209962',//'47247954'
vcId: patient.vcId,
patientName: patient.fullName ?? (patient.firstName != null ? "${patient.firstName} ${patient.lastName}" : "-"),
tokenID: await sharedPref.getString(TOKEN),
generalId: GENERAL_ID,
doctorId: doctorProfile.doctorID,
onFailure: (String error) {
DrAppToastMsg.showErrorToast(error);
},onCallConnected: onCallConnected,
onCallEnd: () {
WidgetsBinding.instance.addPostFrameCallback((_) async {
GifLoaderDialogUtils.showMyDialog(locator<NavigationService>().navigatorKey.currentContext);
endCall(patient.vcId, false,).then((value) {
GifLoaderDialogUtils.hideDialog(locator<NavigationService>().navigatorKey.currentContext);
if (hasError) {
DrAppToastMsg.showErrorToast(error);
}else
locator<NavigationService>().navigateTo(PATIENTS_END_Call,arguments: {
"patient": patient,
});
});
});
},
onCallNotRespond: (SessionStatusModel sessionStatusModel) {
WidgetsBinding.instance.addPostFrameCallback((_) {
GifLoaderDialogUtils.showMyDialog(locator<NavigationService>().navigatorKey.currentContext);
endCall(patient.vcId, sessionStatusModel.sessionStatus == 3,).then((value) {
GifLoaderDialogUtils.hideDialog(locator<NavigationService>().navigatorKey.currentContext);
if (hasError) {
DrAppToastMsg.showErrorToast(error);
} else {
locator<NavigationService>().navigateTo(PATIENTS_END_Call,arguments: {
"patient": patient,
});
}
});
});
});
}
Future endCall(int vCID, bool isPatient) async {
hasError = false;
await getDoctorProfile(isGetProfile: true);
EndCallReq endCallReq = new EndCallReq();
endCallReq.doctorId = doctorProfile.doctorID;
endCallReq.generalid = 'Cs2020@2016\$2958';
endCallReq.vCID = vCID;
endCallReq.isDestroy = isPatient;
await _liveCarePatientServices.endCall(endCallReq);
if (_liveCarePatientServices.hasError) {
error = _liveCarePatientServices.error;
}
}
}

@ -254,8 +254,8 @@ class AuthenticationViewModel extends BaseViewModel {
/// add  token to shared preferences in case of send activation code is success
setDataAfterSendActivationSuccess(SendActivationCodeForDoctorAppResponseModel sendActivationCodeForDoctorAppResponseModel) {
print("VerificationCode : " +
sendActivationCodeForDoctorAppResponseModel.verificationCode);
print("VerificationCode : " + sendActivationCodeForDoctorAppResponseModel.verificationCode);
// DrAppToastMsg.showSuccesToast("VerificationCode : " + sendActivationCodeForDoctorAppResponseModel.verificationCode);
sharedPref.setString(VIDA_AUTH_TOKEN_ID,
sendActivationCodeForDoctorAppResponseModel.vidaAuthTokenID);
sharedPref.setString(VIDA_REFRESH_TOKEN_ID,

@ -240,7 +240,7 @@ class PatientReferralViewModel extends BaseViewModel {
patientID: patient.patientId,
roomID: patient.roomId,
referralClinic: clinicID,
admissionNo: patient.appointmentNo,
admissionNo: int.parse(patient.admissionNo),
referralDoctor: doctorID,
patientTypeID: patient.patientType,
referringDoctorRemarks: remarks,

@ -13,7 +13,7 @@ class ScanQrViewModel extends BaseViewModel {
await getDoctorProfile();
setState(ViewState.Busy);
await _scanQrService.getInPatient(requestModel, false);
await _scanQrService.getInPatient(requestModel, true);
if (_scanQrService.hasError) {
error = _scanQrService.error;

@ -4,6 +4,7 @@ import 'package:doctor_app_flutter/screens/home/home_screen.dart';
import 'package:doctor_app_flutter/screens/qr_reader/QR_reader_screen.dart';
import 'package:doctor_app_flutter/util/translations_delegate_base.dart';
import 'package:doctor_app_flutter/widgets/shared/app_drawer_widget.dart';
import 'package:doctor_app_flutter/widgets/shared/app_scaffold_widget.dart';
import 'package:doctor_app_flutter/widgets/shared/bottom_nav_bar.dart';
import 'package:doctor_app_flutter/widgets/shared/user-guid/app_showcase_widget.dart';
import 'package:flutter/cupertino.dart';
@ -33,7 +34,7 @@ class _LandingPageState extends State<LandingPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
return AppScaffold(
appBar: currentTab != 0
? AppBar(
elevation: 0,

@ -11,6 +11,7 @@ import 'package:doctor_app_flutter/core/viewModel/sick_leave_view_model.dart';
import 'package:get_it/get_it.dart';
import 'core/service/NavigationService.dart';
import 'core/service/VideoCallService.dart';
import 'core/service/home/dasboard_service.dart';
import 'core/service/home/doctor_reply_service.dart';
import 'core/service/home/schedule_service.dart';
@ -96,6 +97,7 @@ void setupLocator() {
locator.registerLazySingleton(() => NavigationService());
locator.registerLazySingleton(() => ScanQrService());
locator.registerLazySingleton(() => SpecialClinicsService());
locator.registerLazySingleton(() => VideoCallService());
/// View Model
locator.registerFactory(() => DoctorReplayViewModel());

@ -224,10 +224,13 @@ class PatiantInformtion {
isSigned: json['isSigned'],
medicationOrders: json['medicationOrders'],
nationality: json['nationality'] ?? json['NationalityNameN'],
patientMRN: json['patientMRN'] ?? json['PatientMRN']?? (
json["PatientID"] != null ?
int.parse(json["PatientID"].toString())
: int.parse(json["patientID"].toString())),
patientMRN: json['patientMRN'] ??
json['PatientMRN'] ??
(json["PatientID"] != null
? int?.parse(json["PatientID"].toString())
: json["patientID"] != null ? int?.parse(
json["patientID"].toString()) : json["patientId"] != null ? int
?.parse(json["patientId"].toString()) : ''),
visitType: json['visitType'] ?? json['visitType'] ?? json['visitType'],
nationalityFlagURL:
json['NationalityFlagURL'] ?? json['NationalityFlagURL'],

@ -1,4 +1,5 @@
import 'package:doctor_app_flutter/root_page.dart';
import 'package:doctor_app_flutter/screens/live_care/end_call_screen.dart';
import 'package:doctor_app_flutter/screens/medical-file/health_summary_page.dart';
import 'package:doctor_app_flutter/screens/patients/ECGPage.dart';
import 'package:doctor_app_flutter/screens/patients/insurance_approval_screen_patient.dart';
@ -37,6 +38,7 @@ const String LOGIN = 'login';
const String VERIFICATION_METHODS = 'verification-methods';
const String PATIENTS = 'patients/patients';
const String PATIENTS_PROFILE = 'patients/patients-profile';
const String PATIENTS_END_Call = 'patients/patients-profile/endCall';
const String IN_PATIENTS_PROFILE = 'inpatients/patients-profile';
const String LAB_RESULT = 'patients/lab_result';
const String HEALTH_SUMMARY = 'patients/health-summary';
@ -88,6 +90,7 @@ var routes = {
PATIENT_MEDICAL_REPORT: (_) => MedicalReportPage(),
PATIENT_MEDICAL_REPORT_INSERT: (_) => AddVerifyMedicalReport(),
PATIENT_MEDICAL_REPORT_DETAIL: (_) => MedicalReportDetailPage(),
PATIENTS_END_Call: (_) => EndCallScreen(),
CREATE_EPISODE: (_) => UpdateSoapIndex(
isUpdate: true,
),

@ -69,7 +69,7 @@ class _HomeScreenState extends State<HomeScreen> {
await model.getDashboard();
await model.getDoctorProfile(isGetProfile: true);
await model.checkDoctorHasLiveCare();
await model.getSpecialClinicalCareList();
// await model.getSpecialClinicalCareList();
},
builder: (_, model, w) => AppScaffold(
baseViewModel: model,

@ -25,7 +25,7 @@ import 'package:hexcolor/hexcolor.dart';
class EndCallScreen extends StatefulWidget {
final PatiantInformtion patient;
const EndCallScreen({Key key, this.patient}) : super(key: key);
const EndCallScreen({Key key, this.patient,}) : super(key: key);
@override
_EndCallScreenState createState() => _EndCallScreenState();
@ -33,7 +33,7 @@ class EndCallScreen extends StatefulWidget {
class _EndCallScreenState extends State<EndCallScreen> {
bool isInpatient = false;
PatiantInformtion patient;
bool isDischargedPatient = false;
bool isSearchAndOut = false;
String patientType;
@ -42,6 +42,20 @@ class _EndCallScreenState extends State<EndCallScreen> {
String to;
LiveCarePatientViewModel liveCareModel;
@override
void initState() {
super.initState();
if(widget.patient!=null)
patient = widget.patient;
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
final routeArgs = ModalRoute.of(context).settings.arguments as Map;
if(routeArgs.containsKey('patient'))
patient = routeArgs['patient'];
}
@override
Widget build(BuildContext context) {
@ -53,7 +67,7 @@ class _EndCallScreenState extends State<EndCallScreen> {
onTap: () async {
GifLoaderDialogUtils.showMyDialog(context);
await liveCareModel
.startCall(isReCall: false, vCID: widget.patient.vcId)
.startCall(isReCall: false, vCID: patient.vcId)
.then((value) async {
await liveCareModel.getDoctorProfile();
GifLoaderDialogUtils.hideDialog(context);
@ -64,8 +78,8 @@ class _EndCallScreenState extends State<EndCallScreen> {
kToken: liveCareModel.startCallRes.openTokenID,
kSessionId: liveCareModel.startCallRes.openSessionID,
kApiKey: '46209962',
vcId: widget.patient.vcId,
patientName: widget.patient.fullName ?? (widget.patient.firstName != null ? "${widget.patient.firstName} ${widget.patient.lastName}" : "-"),
vcId: patient.vcId,
patientName: patient.fullName ?? (patient.firstName != null ? "${patient.firstName} ${patient.lastName}" : "-"),
tokenID: await liveCareModel.getToken(),
generalId: GENERAL_ID,
doctorId: liveCareModel.doctorProfile.doctorID,
@ -76,7 +90,7 @@ class _EndCallScreenState extends State<EndCallScreen> {
GifLoaderDialogUtils.showMyDialog(context);
GifLoaderDialogUtils.showMyDialog(context);
await liveCareModel.endCall(
widget.patient.vcId,
patient.vcId,
false,
);
GifLoaderDialogUtils.hideDialog(context);
@ -89,7 +103,7 @@ class _EndCallScreenState extends State<EndCallScreen> {
GifLoaderDialogUtils.showMyDialog(context);
GifLoaderDialogUtils.showMyDialog(context);
await liveCareModel.endCall(
widget.patient.vcId,
patient.vcId,
sessionStatusModel.sessionStatus == 3,
);
GifLoaderDialogUtils.hideDialog(context);
@ -116,14 +130,14 @@ class _EndCallScreenState extends State<EndCallScreen> {
() async {
Navigator.of(context).pop();
GifLoaderDialogUtils.showMyDialog(context);
await liveCareModel.getAlternativeServices(widget.patient.vcId);
await liveCareModel.getAlternativeServices(patient.vcId);
GifLoaderDialogUtils.hideDialog(context);
if (liveCareModel.state == ViewState.ErrorLocal) {
DrAppToastMsg.showErrorToast(liveCareModel.error);
} else {
showAlternativesDialog(context, liveCareModel, (bool isConfirmed) async {
GifLoaderDialogUtils.showMyDialog(context);
await liveCareModel.endCallWithCharge(widget.patient.vcId, isConfirmed);
await liveCareModel.endCallWithCharge(patient.vcId, isConfirmed);
GifLoaderDialogUtils.hideDialog(context);
if (liveCareModel.state == ViewState.ErrorLocal) {
DrAppToastMsg.showErrorToast(liveCareModel.error);
@ -147,7 +161,7 @@ class _EndCallScreenState extends State<EndCallScreen> {
() async {
Navigator.of(context).pop();
GifLoaderDialogUtils.showMyDialog(context);
await liveCareModel.sendSMSInstruction(widget.patient.vcId);
await liveCareModel.sendSMSInstruction(patient.vcId);
GifLoaderDialogUtils.hideDialog(context);
if (liveCareModel.state == ViewState.ErrorLocal) {
DrAppToastMsg.showErrorToast(liveCareModel.error);
@ -169,7 +183,7 @@ class _EndCallScreenState extends State<EndCallScreen> {
context,
MaterialPageRoute(
builder: (BuildContext context) =>
LivaCareTransferToAdmin(patient: widget.patient)));
LivaCareTransferToAdmin(patient: patient)));
},
isInPatient: isInpatient,
isDartIcon: true,
@ -186,10 +200,14 @@ class _EndCallScreenState extends State<EndCallScreen> {
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
isShowAppBar: true,
appBar: PatientProfileAppBar(
widget.patient,
patient,
onPressed: (){
Navigator.pop(context);
},
isInpatient: isInpatient,
height: (widget.patient.patientStatusType != null &&
widget.patient.patientStatusType == 43)
height: (patient.patientStatusType != null &&
patient.patientStatusType == 43)
? 210
: isDischargedPatient
? 240
@ -232,7 +250,7 @@ class _EndCallScreenState extends State<EndCallScreen> {
staggeredTileBuilder: (int index) => StaggeredTile.fit(1),
itemBuilder: (BuildContext context, int index) =>
PatientProfileButton(
patient: widget.patient,
patient: patient,
patientType: patientType,
arrivalType: arrivalType,
from: from,
@ -331,6 +349,7 @@ class _EndCallScreenState extends State<EndCallScreen> {
),
AppButton(
onPressed: () {
Navigator.of(context).pop();
Navigator.of(context).pop();
okFunction(false);
},

@ -65,13 +65,12 @@ class _PatientInPatientScreenState extends State<PatientInPatientScreen>
return BaseView<PatientSearchViewModel>(
onModelReady: (model) async {
model.clearPatientList();
if (widget.specialClinic != null) {
await model
.getSpecialClinicalCareMappingList(widget.specialClinic.clinicID);
requestModel.nursingStationID =
model.specialClinicalCareMappingList[0].nursingStationID;
requestModel.clinicID = 0;
}
// if (widget.specialClinic != null) {
// await model.getSpecialClinicalCareMappingList(widget.specialClinic.clinicID);
// requestModel.nursingStationID =
// model.specialClinicalCareMappingList[0].nursingStationID;
// requestModel.clinicID = 0;
// }
model.getInPatientList(requestModel);
},
builder: (_, model, w) =>

@ -1,5 +1,8 @@
import 'dart:async';
import 'package:doctor_app_flutter/config/config.dart';
import 'package:doctor_app_flutter/core/enum/viewstate.dart';
import 'package:doctor_app_flutter/core/service/VideoCallService.dart';
import 'package:doctor_app_flutter/core/viewModel/LiveCarePatientViewModel.dart';
import 'package:doctor_app_flutter/core/viewModel/SOAP_view_model.dart';
import 'package:doctor_app_flutter/models/SOAP/PostEpisodeReqModel.dart';
@ -10,17 +13,21 @@ import 'package:doctor_app_flutter/screens/live_care/end_call_screen.dart';
import 'package:doctor_app_flutter/screens/patients/profile/profile_screen/profile_gird_for_InPatient.dart';
import 'package:doctor_app_flutter/screens/patients/profile/profile_screen/profile_gird_for_other.dart';
import 'package:doctor_app_flutter/screens/patients/profile/profile_screen/profile_gird_for_search.dart';
import 'package:doctor_app_flutter/util/NotificationPermissionUtils.dart';
import 'package:doctor_app_flutter/util/VideoChannel.dart';
import 'package:doctor_app_flutter/util/dr_app_toast_msg.dart';
import 'package:doctor_app_flutter/util/helpers.dart';
import 'package:doctor_app_flutter/util/translations_delegate_base.dart';
import 'package:doctor_app_flutter/widgets/patients/profile/patient-profile-app-bar.dart';
import 'package:doctor_app_flutter/widgets/patients/profile/patient-profile-header-new-design-app-bar.dart';
import 'package:doctor_app_flutter/widgets/shared/app_scaffold_widget.dart';
import 'package:doctor_app_flutter/widgets/shared/buttons/app_buttons_widget.dart';
import 'package:doctor_app_flutter/widgets/shared/loader/gif_loader_dialog_utils.dart';
import 'package:flutter/material.dart';
import 'package:hexcolor/hexcolor.dart';
import 'package:quiver/async.dart';
import '../../../../locator.dart';
import '../../../../routes.dart';
class PatientProfileScreen extends StatefulWidget {
@ -46,10 +53,15 @@ class _PatientProfileScreenState extends State<PatientProfileScreen>
TabController _tabController;
int index = 0;
int _activeTab = 0;
StreamController<String> videoCallDurationStreamController;
Stream<String> videoCallDurationStream = (() async*{})();
@override
void initState() {
_tabController = TabController(length: 2, vsync: this);
super.initState();
videoCallDurationStreamController = StreamController();
videoCallDurationStream = videoCallDurationStreamController.stream;
}
@override
@ -82,12 +94,32 @@ class _PatientProfileScreenState extends State<PatientProfileScreen>
if(routeArgs.containsKey("isFromLiveCare")) {
isFromLiveCare = routeArgs['isFromLiveCare'];
}
if(routeArgs.containsKey("isCallFinished")) {
isCallFinished = routeArgs['isCallFinished'];
}
if (isInpatient)
_activeTab = 0;
else
_activeTab = 1;
}
StreamSubscription callTimer;
callConnected(){
callTimer = CountdownTimer(Duration(minutes: 90), Duration(seconds: 1)).listen(null)
..onDone(() {
callTimer.cancel();
})
..onData((data) {
var t = Helpers.timeFrom(duration: data.elapsed);
videoCallDurationStreamController.sink.add(t);
});
}
callDisconnected(){
callTimer.cancel();
videoCallDurationStreamController.sink.add(null);
}
@override
Widget build(BuildContext context) {
final screenSize = MediaQuery.of(context).size;
@ -102,17 +134,18 @@ class _PatientProfileScreenState extends State<PatientProfileScreen>
children: [
Column(
children: [
PatientProfileAppBar(
patient,
isFromLiveCare: isFromLiveCare,
isInpatient: isInpatient,
height: (patient.patientStatusType != null &&
patient.patientStatusType == 43)
? 220
: isDischargedPatient
? 240
: 0,
isDischargedPatient: isDischargedPatient),
PatientProfileHeaderNewDesignAppBar(
patient, arrivalType ?? '0', patientType,
videoCallDurationStream: videoCallDurationStream,
isInpatient: isInpatient,
isFromLiveCare: isFromLiveCare,
height: (patient.patientStatusType != null &&
patient.patientStatusType == 43)
? 210
: isDischargedPatient
? 240
: 0,
isDischargedPatient: isDischargedPatient),
Container(
height: !isSearchAndOut
? isDischargedPatient
@ -307,65 +340,29 @@ class _PatientProfileScreenState extends State<PatientProfileScreen>
if(isCallFinished) {
Navigator.push(context, MaterialPageRoute(
builder: (BuildContext context) =>
EndCallScreen(patient:patient)));
builder: (BuildContext context) => EndCallScreen(patient:patient)));
} else {
GifLoaderDialogUtils.showMyDialog(context);
await model.startCall( isReCall : false, vCID: patient.vcId);
await model.startCall( isReCall : false, vCID: patient.vcId);
if(model.state == ViewState.ErrorLocal) {
GifLoaderDialogUtils.hideDialog(context);
Helpers.showErrorToast(model.error);
} else {
await model.getDoctorProfile();
patient.appointmentNo = model.startCallRes.appointmentNo;
patient.episodeNo = 0;
if(model.state == ViewState.ErrorLocal) {
GifLoaderDialogUtils.hideDialog(context);
Helpers.showErrorToast(model.error);
} else {
await model.getDoctorProfile();
patient.appointmentNo = model.startCallRes.appointmentNo;
patient.episodeNo = 0;
GifLoaderDialogUtils.hideDialog(context);
await VideoChannel.openVideoCallScreen(
kToken: model.startCallRes.openTokenID,
kSessionId: model.startCallRes.openSessionID,
kApiKey: '46209962',
vcId: patient.vcId,
patientName: patient.fullName ?? (patient.firstName != null ? "${patient.firstName} ${patient.lastName}" : "-"),
tokenID: await model.getToken(),
generalId: GENERAL_ID,
doctorId: model.doctorProfile.doctorID,
onFailure: (String error) {
DrAppToastMsg.showErrorToast(error);
},
onCallEnd: () {
WidgetsBinding.instance.addPostFrameCallback((_) {
GifLoaderDialogUtils.showMyDialog(context);
model.endCall(patient.vcId, false,).then((value) {
GifLoaderDialogUtils.hideDialog(context);
if (model.state == ViewState.ErrorLocal) {
DrAppToastMsg.showErrorToast(model.error);
}
setState(() {
isCallFinished = true;
});
});
});
},
onCallNotRespond: (SessionStatusModel sessionStatusModel) {
WidgetsBinding.instance.addPostFrameCallback((_) {
GifLoaderDialogUtils.showMyDialog(context);
model.endCall(patient.vcId, sessionStatusModel.sessionStatus == 3,).then((value) {
GifLoaderDialogUtils.hideDialog(context);
if (model.state == ViewState.ErrorLocal) {
DrAppToastMsg.showErrorToast(model.error);
}
setState(() {
isCallFinished = true;
});
});
GifLoaderDialogUtils.hideDialog(context);
AppPermissionsUtils.requestVideoCallPermission(context: context,onTapGrant: (){
locator<VideoCallService>().openVideo(model.startCallRes, patient, callConnected, callDisconnected);
});
});
});
}
}
}
},
),
),

@ -18,7 +18,7 @@ class ProcedureCard extends StatelessWidget {
final int categoryID;
final PatiantInformtion patient;
final int doctorID;
final bool isInpatient;
const ProcedureCard({
Key key,
this.onTap,
@ -26,7 +26,7 @@ class ProcedureCard extends StatelessWidget {
this.categoryID,
this.categoryName,
this.patient,
this.doctorID,
this.doctorID, this.isInpatient = false,
}) : super(key: key);
@override
@ -132,36 +132,6 @@ class ProcedureCard extends StatelessWidget {
),
],
),
// Row(
// children: [
// AppText(
// TranslationBase.of(context).doctorName + ": ",
// //color: Colors.grey,
// fontSize: 12,
// color: Colors.grey,
// ),
// AppText(
// entityList.doctorName.toString(),
// fontSize: 12,
// bold: true,
// ),
// ],
// ),
// Row(
// children: [
// AppText(
// TranslationBase.of(context).clinic + ": ",
// //color: Colors.grey,
// fontSize: 12,
// color: Colors.grey,
// ),
// AppText(
// entityList.clinicDescription ?? "",
// bold: true,
// fontSize: 12,
// ),
// ],
// ),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -222,27 +192,6 @@ class ProcedureCard extends StatelessWidget {
),
],
),
/*Container(
alignment: Alignment.centerRight,
child: InkWell(
onTap: () {
Navigator.push(
context,
FadePage(
page: FlowChartPage(
filterName: entityList.procedureName,
patient: patient,
),
),
);
},
child: AppText(
TranslationBase.of(context).showMoreBtn,
textDecoration: TextDecoration.underline,
color: Colors.blue,
),
),
),*/
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
@ -254,9 +203,7 @@ class ProcedureCard extends StatelessWidget {
fontSize: 12,
),
),
if ((entityList.categoryID == 2 ||
entityList.categoryID == 4) &&
doctorID == entityList.doctorID)
if ((entityList.categoryID == 2 || entityList.categoryID == 4) && doctorID == entityList.doctorID && !isInpatient)
InkWell(
child: Icon(DoctorApp.edit),
onTap: onTap,

@ -0,0 +1,42 @@
import 'package:doctor_app_flutter/widgets/dialog/AskPermissionDialog.dart';
import 'package:doctor_app_flutter/widgets/transitions/slide_up_page.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
class AppPermissionsUtils {
static requestVideoCallPermission({BuildContext context, String type,Function onTapGrant}) async {
var cameraPermission = Permission.camera;
var microphonePermission = Permission.microphone;
PermissionStatus permissionCameraStatus = await cameraPermission.status;
PermissionStatus permissionMicrophoneStatus = await microphonePermission.status;
if (permissionCameraStatus.isPermanentlyDenied || permissionMicrophoneStatus.isPermanentlyDenied) {
await _showPermissionDialog(context, type,onTapGrant);
} else if (!permissionCameraStatus.isGranted || !permissionMicrophoneStatus.isGranted) {
permissionCameraStatus = await cameraPermission.request();
permissionMicrophoneStatus = await microphonePermission.request();
if (permissionCameraStatus.isDenied || permissionMicrophoneStatus.isDenied)
await _showPermissionDialog(context, type,onTapGrant);
else
onTapGrant();
} else if (permissionCameraStatus.isDenied || permissionMicrophoneStatus.isDenied)
await _showPermissionDialog(context, type,onTapGrant);
else
onTapGrant();
}
static _showPermissionDialog(BuildContext context, String type,Function onTapGrant) async {
Navigator.push(
context, SlideUpPageRoute(widget: AskPermissionDialog(type: type,onTapGrant: onTapGrant,)));
}
static Future<bool> isVideoCallPermissionGranted() async {
PermissionStatus permissionCameraStatus = await Permission.camera.status;
PermissionStatus permissionMicrophoneStatus = await Permission.microphone.status;
return permissionCameraStatus.isGranted && permissionMicrophoneStatus.isGranted;
}
}

@ -4,15 +4,30 @@ import 'dart:io' show Platform;
import 'package:doctor_app_flutter/config/config.dart';
import 'package:doctor_app_flutter/models/livecare/session_status_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class VideoChannel{
/// channel name
static const _channel = const MethodChannel("Dr.cloudSolution/videoCall");
static openVideoCallScreen(
{kApiKey, kSessionId, kToken, callDuration, warningDuration,int vcId,String tokenID,String generalId,int doctorId, String patientName, Function() onCallEnd , Function(SessionStatusModel sessionStatusModel) onCallNotRespond ,Function(String error) onFailure}) async {
static openVideoCallScreen({kApiKey, kSessionId, kToken, callDuration, warningDuration,int vcId,String tokenID,
String generalId,int doctorId, String patientName, Function() onCallEnd ,
Function(SessionStatusModel sessionStatusModel) onCallNotRespond ,Function(String error) onFailure, VoidCallback onCallConnected, VoidCallback onCallDisconnected}) async {
onCallConnected = onCallConnected ?? (){};
onCallDisconnected = onCallDisconnected ?? (){};
var result;
try {
_channel.setMethodCallHandler((call) {
if(call.method == 'onCallConnected'){
onCallConnected();
}
if(call.method == 'onCallDisconnected'){
onCallDisconnected();
}
return true as dynamic;
});
result = await _channel.invokeMethod(
'openVideoCall',
{

@ -270,4 +270,11 @@ class Helpers {
var htmlRegex = RegExp("<(“[^”]*”|'[^]*|[^'”>])*>");
return htmlRegex.hasMatch(text);
}
static String timeFrom({Duration duration}) {
String twoDigits(int n) => n.toString().padLeft(2, "0");
String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60));
String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60));
return "$twoDigitMinutes:$twoDigitSeconds";
}
}

@ -0,0 +1,90 @@
import 'package:doctor_app_flutter/widgets/shared/app_texts_widget.dart';
import 'package:doctor_app_flutter/widgets/shared/buttons/app_buttons_widget.dart';
import 'package:doctor_app_flutter/widgets/shared/buttons/secondary_button.dart';
import 'package:eva_icons_flutter/eva_icons_flutter.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
class AskPermissionDialog extends StatefulWidget {
final String type;
final Function onTapGrant;
AskPermissionDialog({this.type, this.onTapGrant});
@override
_AskPermissionDialogState createState() => _AskPermissionDialogState();
}
class _AskPermissionDialogState extends State<AskPermissionDialog> {
getText() {
return "Turn on your Camera, Microphone to start video call";
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
elevation: 0.5,
actions: <Widget>[
Padding(
padding: EdgeInsets.only(right: 18.0),
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: Feedback.wrapForTap(() {
Navigator.pop(context);
}, context),
child:
Icon(EvaIcons.close, color: Theme.of(context).primaryColor),
),
)
],
),
body: Container(
margin: EdgeInsets.symmetric(horizontal: 48),
child: Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
AppText(
"🙋‍♀",
style: "headline1",
color: Colors.black,
),
AppText(
"Don't miss out!",
style: "headline4",
color: Colors.black,
),
SizedBox(
height: 8,
),
AppText(
getText(),
color: Colors.grey,
style: "bodyText2",
textAlign: TextAlign.center,
),
SizedBox(
height: MediaQuery.of(context).size.height / 6,
),
AppButton(
fontColor: Theme.of(context).backgroundColor,
color: Colors.red[700],
title: "Turn On Camera, Microphone",
onPressed: () async {
openAppSettings().then((value) {
Navigator.pop(context);
widget.onTapGrant();
});
},
),
],
),
),
),
);
}
}

@ -33,6 +33,7 @@ class PatientProfileAppBar extends StatelessWidget
final String clinic;
final bool isAppointmentHeader;
final bool isFromLabResult;
final VoidCallback onPressed;
PatientProfileAppBar(
this.patient,
@ -52,7 +53,7 @@ class PatientProfileAppBar extends StatelessWidget
this.episode,
this.visitDate,
this.isAppointmentHeader = false,
this.isFromLabResult = false});
this.isFromLabResult = false, this.onPressed});
@override
@ -93,7 +94,11 @@ class PatientProfileAppBar extends StatelessWidget
IconButton(
icon: Icon(Icons.arrow_back_ios),
color: Color(0xFF2B353E), //Colors.black,
onPressed: () => Navigator.pop(context),
onPressed: () {
if(onPressed!=null)
onPressed();
Navigator.pop(context);
},
),
Expanded(
child: AppText(

@ -0,0 +1,390 @@
import 'package:doctor_app_flutter/config/config.dart';
import 'package:doctor_app_flutter/config/size_config.dart';
import 'package:doctor_app_flutter/icons_app/doctor_app_icons.dart';
import 'package:doctor_app_flutter/models/patient/patiant_info_model.dart';
import 'package:doctor_app_flutter/util/date-utils.dart';
import 'package:doctor_app_flutter/util/helpers.dart';
import 'package:doctor_app_flutter/util/translations_delegate_base.dart';
import 'package:doctor_app_flutter/widgets/shared/app_texts_widget.dart';
import 'package:flutter/material.dart';
import 'package:hexcolor/hexcolor.dart';
import 'package:intl/intl.dart';
import 'package:url_launcher/url_launcher.dart';
class PatientProfileHeaderNewDesignAppBar extends StatelessWidget
with PreferredSizeWidget {
final PatiantInformtion patient;
final String patientType;
final String arrivalType;
final double height;
final bool isInpatient;
final bool isDischargedPatient;
final bool isFromLiveCare;
final Stream<String> videoCallDurationStream;
PatientProfileHeaderNewDesignAppBar(
this.patient, this.patientType, this.arrivalType, {this.height = 0.0, this.isInpatient=false, this.isDischargedPatient=false, this.isFromLiveCare = false, this.videoCallDurationStream});
@override
Widget build(BuildContext context) {
int gender = 1;
if (patient.patientDetails != null) {
gender = patient.patientDetails.gender;
} else {
gender = patient.gender;
}
return Container(
padding: EdgeInsets.only(
left: 0,
right: 5,
bottom: 5,
),
decoration: BoxDecoration(
color: Colors.white,
),
height: height == 0 ? isInpatient? 215:200 : height,
child: Container(
padding: EdgeInsets.only(left: 10, right: 10, bottom: 10),
margin: EdgeInsets.only(top: 50),
child: Column(
children: [
Container(
padding: EdgeInsets.only(left: 12.0),
child: Row(children: [
IconButton(
icon: Icon(Icons.arrow_back_ios),
color: Colors.black, //Colors.black,
onPressed: () => Navigator.pop(context),
),
Expanded(
child: AppText(
patient.firstName != null
? (Helpers.capitalize(patient.firstName) +
" " +
Helpers.capitalize(patient.lastName))
: Helpers.capitalize(patient.fullName??patient.patientDetails.fullName),
fontSize: SizeConfig.textMultiplier * 1.8,
fontWeight: FontWeight.bold,
fontFamily: 'Poppins',
),
),
gender == 1
? Icon(
DoctorApp.male_2,
color: Colors.blue,
)
: Icon(
DoctorApp.female_1,
color: Colors.pink,
),
Container(
margin: EdgeInsets.symmetric(horizontal: 4),
child: InkWell(
onTap: () {
launch("tel://" + patient.mobileNumber);
},
child: Icon(
Icons.phone,
color: Colors.black87,
),
),
),
StreamBuilder(
stream: videoCallDurationStream,
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
if(snapshot.hasData && snapshot.data != null)
return InkWell(
onTap: (){
},
child: Container(
decoration: BoxDecoration(color: Colors.red, borderRadius: BorderRadius.circular(20)),
padding: EdgeInsets.symmetric(vertical: 2, horizontal: 10),
child: Text(snapshot.data, style: TextStyle(color: Colors.white),),
),
);
else
return Container();
},
),
]),
),
Row(children: [
Padding(
padding: EdgeInsets.only(left: 12.0),
child: Container(
width: 60,
height: 60,
child: Image.asset(
gender == 1
? 'assets/images/male_avatar.png'
: 'assets/images/female_avatar.png',
fit: BoxFit.cover,
),
),
),
SizedBox(
width: 10,
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SERVICES_PATIANT2[int.parse(patientType)] ==
"patientArrivalList"
? Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
patient.patientStatusType == 43
? AppText(
TranslationBase.of(context).arrivedP,
color: Colors.green,
fontWeight: FontWeight.bold,
fontFamily: 'Poppins',
fontSize: 12,
)
: AppText(
TranslationBase.of(context).notArrived,
color: Colors.red[800],
fontWeight: FontWeight.bold,
fontFamily: 'Poppins',
fontSize: 12,
),
arrivalType == '1'|| patient.arrivedOn == null
? AppText(
patient.startTime != null
? patient.startTime
: '',
fontFamily: 'Poppins',
fontWeight: FontWeight.w600,
)
: AppText(
patient.arrivedOn != null
? AppDateUtils.convertStringToDateFormat(
patient.arrivedOn,
'MM-dd-yyyy HH:mm')
: '',
fontFamily: 'Poppins',
fontWeight: FontWeight.w600,
)
],
))
: SizedBox(),
if (SERVICES_PATIANT2[int.parse(patientType)] ==
"List_MyOutPatient" && !isFromLiveCare)
Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
AppText(
TranslationBase.of(context).appointmentDate +
" : ",
fontSize: 14,
),
patient.startTime != null
? Container(
height: 15,
width: 60,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(25),
color: HexColor("#20A169"),
),
child: AppText(
patient.startTime??"",
color: Colors.white,
fontSize: 1.5 * SizeConfig.textMultiplier,
textAlign: TextAlign.center,
fontWeight: FontWeight.bold,
),
)
: SizedBox(),
SizedBox(
width: 3.5,
),
Container(
child: AppText(
convertDateFormat2(
patient.appointmentDate ?? ''),
fontSize: 1.5 * SizeConfig.textMultiplier,
fontWeight: FontWeight.bold,
),
),
SizedBox(
height: 0.5,
)
],
),
margin: EdgeInsets.only(
top: 8,
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
RichText(
text: TextSpan(
style: TextStyle(
fontSize: 1.6 * SizeConfig.textMultiplier,
color: Colors.black),
children: <TextSpan>[
new TextSpan(
text: TranslationBase.of(context).fileNumber,
style: TextStyle(
fontSize: 12, fontFamily: 'Poppins')),
new TextSpan(
text: patient.patientId.toString(),
style: TextStyle(
fontWeight: FontWeight.w700,
fontFamily: 'Poppins',
fontSize: 14)),
],
),
),
Row(
children: [
AppText(
patient.nationalityName ?? patient.nationality?? patient.nationalityId ?? '',
fontWeight: FontWeight.bold,
fontSize: 12,
),
patient.nationalityFlagURL != null
? ClipRRect(
borderRadius: BorderRadius.circular(20.0),
child: Image.network(
patient.nationalityFlagURL,
height: 25,
width: 30,
errorBuilder: (BuildContext context,
Object exception,
StackTrace stackTrace) {
return Text('No Image');
},
))
: SizedBox()
],
)
],
),
Container(
child: RichText(
text: new TextSpan(
style: new TextStyle(
fontSize: 1.6 * SizeConfig.textMultiplier,
color: Colors.black,
fontFamily: 'Poppins',
),
children: <TextSpan>[
new TextSpan(
text: TranslationBase.of(context).age+ " : ",
style: TextStyle(fontSize: 14)),
new TextSpan(
text:
"${AppDateUtils.getAgeByBirthday(patient.patientDetails != null ? patient.patientDetails.dateofBirth??"" : patient.dateofBirth??"", context,isServerFormat: !isFromLiveCare)}",
style: TextStyle(
fontWeight: FontWeight.w700, fontSize: 14)),
],
),
),
),
if(isInpatient)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
child: RichText(
text: new TextSpan(
style: new TextStyle(
fontSize:
2.0 * SizeConfig.textMultiplier,
color: Colors.black,
fontFamily: 'Poppins',
),
children: <TextSpan>[
new TextSpan(
text: patient.admissionDate == null
? ""
: TranslationBase.of(context)
.admissionDate +
" : ",
style: TextStyle(fontSize: 14)),
new TextSpan(
text: patient.admissionDate == null
? ""
: "${AppDateUtils.convertDateFromServerFormat(patient.admissionDate.toString(), 'yyyy-MM-dd')}",
style: TextStyle(
fontWeight: FontWeight.w700,
fontSize: 15)),
]))),
if (patient.admissionDate != null)
Row(
children: [
AppText(
"${TranslationBase.of(context).numOfDays}: ",
fontSize: 15,
),
if(isDischargedPatient && patient.dischargeDate!=null)
AppText(
"${AppDateUtils.getDateTimeFromServerFormat(patient.dischargeDate).difference(AppDateUtils.getDateTimeFromServerFormat(patient.admissionDate)).inDays + 1}",
fontSize: 15,
fontWeight: FontWeight.w700)
else
AppText(
"${DateTime.now().difference(AppDateUtils.getDateTimeFromServerFormat(patient.admissionDate)).inDays + 1}",
fontSize: 15,
fontWeight: FontWeight.w700),
],
),
],
)
],
),
),
]),
],
),
),
);
}
convertDateFormat2(String str) {
String newDate;
const start = "/Date(";
if (str.isNotEmpty) {
const end = "+0300)";
final startIndex = str.indexOf(start);
final endIndex = str.indexOf(end, startIndex + start.length);
var date = new DateTime.fromMillisecondsSinceEpoch(
int.parse(str.substring(startIndex + start.length, endIndex)));
newDate = date.year.toString() +
"/" +
date.month.toString().padLeft(2, '0') +
"/" +
date.day.toString().padLeft(2, '0');
}
return newDate??'';
}
isToday(date) {
DateTime tempDate = new DateFormat("yyyy-MM-dd").parse(date);
return DateFormat("yyyy-MM-dd").format(tempDate) ==
DateFormat("yyyy-MM-dd").format(DateTime.now());
}
myBoxDecoration() {
return BoxDecoration(
border: Border(
top: BorderSide(
color: Colors.green,
width: 5,
),
),
borderRadius: BorderRadius.circular(10));
}
@override
Size get preferredSize => Size(double.maxFinite, 200);
}

@ -19,8 +19,11 @@ class AppScaffold extends StatelessWidget {
final Widget bottomSheet;
final Color backgroundColor;
final Widget appBar;
final Widget drawer;
final Widget bottomNavigationBar;
final String subtitle;
final bool isHomeIcon;
final bool extendBody;
AppScaffold(
{this.appBarTitle = '',
this.body,
@ -30,7 +33,7 @@ class AppScaffold extends StatelessWidget {
this.bottomSheet,
this.backgroundColor,
this.isHomeIcon = true,
this.appBar, this.subtitle});
this.appBar, this.subtitle, this.drawer, this.extendBody = false, this.bottomNavigationBar});
@override
Widget build(BuildContext context) {
@ -42,6 +45,9 @@ class AppScaffold extends StatelessWidget {
},
child: Scaffold(
backgroundColor: backgroundColor ?? Colors.white,
drawer: drawer,
extendBody: extendBody,
bottomNavigationBar: bottomNavigationBar,
appBar: isShowAppBar
? appBar ??
AppBar(

@ -587,7 +587,7 @@ packages:
name: js
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.3-nullsafety.1"
version: "0.6.2"
json_annotation:
dependency: transitive
description:
@ -813,7 +813,7 @@ packages:
source: hosted
version: "0.1.8"
quiver:
dependency: transitive
dependency: "direct main"
description:
name: quiver
url: "https://pub.dartlang.org"

@ -90,7 +90,7 @@ dependencies:
speech_to_text:
path: speech_to_text
quiver: ^2.1.5
# Html Editor Enhanced
html_editor_enhanced: ^1.3.0

Loading…
Cancel
Save