Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Xamarin samples for Android and iOS #3584

Merged
merged 5 commits into from
Jun 16, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions pjsip-apps/src/swig/csharp/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,13 @@ NAMESPACE=$(PROJ_NAME).pjsua2
ARCH=$(TARGET_ARCH)

ifeq ($(OS),android)
LIBPJSUA2_DIR=$(PROJ_NAME)/Droid/lib/$(ARCH)
LIBPJSUA2_DIR=$(PROJ_NAME)/$(PROJ_NAME).Android/lib/$(ARCH)
LIBPJSUA2=$(LIBPJSUA2_DIR)/libpjsua2.so
SUP_CLASS_DIR=$(PROJ_NAME)/$(PROJ_NAME).Android/org/pjsip
else
ifeq ($(OS),ios)
LIBPJSUA2_DIR=$(PROJ_NAME)/iOS/lib/$(ARCH)
LIBPJSUA2_DIR=$(PROJ_NAME)/$(PROJ_NAME).iOS/lib/$(ARCH)
SWIG_FLAGS += -dllimport "__Internal"
else
LIBPJSUA2_DIR=$(PROJ_NAME)/lib
endif
Expand All @@ -47,13 +49,22 @@ all: $(LIBPJSUA2) sample
$(LIBPJSUA2): $(OUT_DIR)/pjsua2_wrap.o
mkdir -p $(LIBPJSUA2_DIR)
ifeq ($(OS),android)
mkdir -p $(SUP_CLASS_DIR)
$(PJ_CXX) -shared -o $(LIBPJSUA2) $(OUT_DIR)/pjsua2_wrap.o \
$(MY_CFLAGS) $(MY_LDFLAGS)
# copy libc++_shared.so manually
cp -f ${STD_CPP_LIB} $(LIBPJSUA2_DIR)
else
$(AR) $(LIBPJSUA2) $(AR_FLAGS) $(OUT_DIR)/pjsua2_wrap.o $(PJ_LIBXX_FILES)
endif
ifneq (,$(findstring PJMEDIA_VIDEO_DEV_HAS_ANDROID=1,$(ANDROID_CFLAGS)))
@echo "Copying Android camera helper components..."
cp $(PJDIR)/pjmedia/src/pjmedia-videodev/android/PjCamera*.java $(SUP_CLASS_DIR)/
endif
ifneq (,$(findstring PJMEDIA_AUDIO_DEV_HAS_OBOE=1,$(OBOE_CFLAGS)))
@echo "Copying Android Oboe audio device helper components..."
cp $(PJDIR)/pjmedia/src/pjmedia-audiodev/android/PjAudioDevInfo.java $(SUP_CLASS_DIR)/
endif

$(OUT_DIR)/pjsua2_wrap.o: $(OUT_DIR)/pjsua2_wrap.cpp
$(PJ_CXX) -c $(OUT_DIR)/pjsua2_wrap.cpp -o $(OUT_DIR)/pjsua2_wrap.o \
Expand All @@ -62,14 +73,15 @@ $(OUT_DIR)/pjsua2_wrap.o: $(OUT_DIR)/pjsua2_wrap.cpp
$(OUT_DIR)/pjsua2_wrap.cpp: ../pjsua2.i ../symbols.i Makefile $(SRCS)
mkdir -p $(OUT_DIR)
swig $(SWIG_FLAGS) -namespace $(NAMESPACE) -csharp -o $(OUT_DIR)/pjsua2_wrap.cpp ../pjsua2.i

sample: sample.cs
@echo "Copying sample code..."
cp sample.cs $(PROJ_NAME)/$(PROJ_NAME)

clean distclean realclean:
rm -rf ./$(OUT_DIR)/*
rm -rf ./$(LIBPJSUA2_DIR)/*
rm -rf ./$(SUP_CLASS_DIR)/*.java

install:

Expand Down
6 changes: 6 additions & 0 deletions pjsip-apps/src/swig/csharp/pjsua2xamarin/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.dll
.java
.class
.jar
.so
/*/obj/
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Any raw assets you want to be deployed with your application can be placed in
this directory (and child directories) and given a Build Action of "AndroidAsset".

These files will be deployed with your package and will be accessible using Android's
AssetManager, like this:

public class ReadAsset : Activity
{
protected override void OnCreate (Bundle bundle)
{
base.OnCreate (bundle);

InputStream input = Assets.Open ("my_asset.txt");
}
}

Additionally, some Android functions will automatically load asset files:

Typeface tf = Typeface.CreateFromAsset (Context.Assets, "fonts/samplefont.ttf");

Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
using System;
using System.IO;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
using pjsua2xamarin;
using pjsua2xamarin.Droid;
using Android.App;
using Android.Content;
using Android.Hardware;
using Android.Views;
using Android.Graphics;
using Android.Widget;
using Android.Renderscripts;
using static Xamarin.Essentials.Permissions;
using pjsua2xamarin.pjsua2;
using AndroidX.Lifecycle;
using System.Runtime.InteropServices;
using Android.Runtime;

[assembly: ExportRenderer(typeof(CallPage), typeof(CallPageRenderer))]
namespace pjsua2xamarin.Droid
{
public class CallPageRenderer : PageRenderer, ISurfaceHolderCallback
{
global::Android.Widget.Button acceptCallButton;
global::Android.Widget.Button hangupCallButton;
global::Android.Widget.TextView peerTxt;
global::Android.Widget.TextView statusTxt;
global::Android.Views.View view;
private static CallInfo lastCallInfo;
private CallPage callPage;
SurfaceView incomingView;

[DllImport("android")]
private static extern IntPtr ANativeWindow_fromSurface(IntPtr jni, IntPtr surface);

public CallPageRenderer(Context context) : base(context)
{
MessagingCenter.Subscribe<BuddyPage, CallInfo>
(this, "UpdateCallState", (obj, info) => {
if (callPage == null)
return;

lastCallInfo = info as CallInfo;
if (lastCallInfo.state == pjsip_inv_state.PJSIP_INV_STATE_DISCONNECTED) {
Device.BeginInvokeOnMainThread(() => {
callPage.Navigation.PopAsync();
});
} else {
Device.BeginInvokeOnMainThread(() => {
updateCallState(lastCallInfo);
});
}
});
MessagingCenter.Subscribe<BuddyPage, CallInfo>
(this, "UpdateMediaCallState", (obj, info) => {
lastCallInfo = info as CallInfo;

if (MyApp.currentCall.vidWin != null) {
incomingView.Visibility = ViewStates.Visible;
}
});
}

~CallPageRenderer() {
MessagingCenter.Unsubscribe<BuddyPage, CallInfo>(this, "UpdateCallState");
MessagingCenter.Unsubscribe<BuddyPage, CallInfo>(this, "UpdateMediaCallState");
}

protected override void OnLayout(bool changed, int l, int t, int r, int b)
{
base.OnLayout(changed, l, t, r, b);

var msw = MeasureSpec.MakeMeasureSpec(r - l, MeasureSpecMode.Exactly);
var msh = MeasureSpec.MakeMeasureSpec(b - t, MeasureSpecMode.Exactly);

view.Measure(msw, msh);
view.Layout(0, 0, r - l, b - t);
}

protected override void OnElementChanged(ElementChangedEventArgs<Page> e)
{
base.OnElementChanged(e);

if (e.OldElement != null || Element == null) {
return;
}

try {
SetupUserInterface();
SetupEventHandlers();
AddView(view);
callPage = (CallPage)Element;
System.Diagnostics.Debug.WriteLine(@"Call page done initialize");
if (MyApp.currentCall != null) {
try {
lastCallInfo = MyApp.currentCall.getInfo();
} catch (Exception ex) {
System.Diagnostics.Debug.WriteLine(@"ERROR: ", ex.Message);
}
Device.BeginInvokeOnMainThread(() => {
updateCallState(lastCallInfo);
});
}
} catch (Exception ex) {
System.Diagnostics.Debug.WriteLine(@"ERROR: ", ex.Message);
}
}

void SetupUserInterface()
{
var activity = this.Context as Activity;
view = activity.LayoutInflater.Inflate(Resource.Layout.activity_call, this, false);

incomingView = view.FindViewById<SurfaceView>(Resource.Id.incomingVideoView);
incomingView.Holder.AddCallback(this);

peerTxt = view.FindViewById<TextView>(Resource.Id.peerTxt);
statusTxt = view.FindViewById<TextView>(Resource.Id.statusTxt);

if (MyApp.currentCall == null || MyApp.currentCall.vidWin == null) {
incomingView.Visibility = ViewStates.Gone;
}
}

void SetupEventHandlers()
{
acceptCallButton = view.FindViewById<global::Android.Widget.Button>(Resource.Id.acceptCallButton);
acceptCallButton.Click += AcceptCallButtonTapped;

hangupCallButton = view.FindViewById<global::Android.Widget.Button>(Resource.Id.hangupCallButton);
hangupCallButton.Click += HangupCallButtonTapped;
}

void AcceptCallButtonTapped(object sender, EventArgs e)
{
CallOpParam prm = new CallOpParam();
prm.statusCode = pjsip_status_code.PJSIP_SC_OK;
try {
MyApp.currentCall.answer(prm);
} catch (Exception ex) {
System.Diagnostics.Debug.WriteLine(@"ERROR: ", ex.Message);
}

acceptCallButton.Visibility = ViewStates.Gone;
}

void HangupCallButtonTapped(object sender, EventArgs e)
{
if (MyApp.currentCall != null) {
CallOpParam prm = new CallOpParam();
prm.statusCode = pjsip_status_code.PJSIP_SC_DECLINE;
try {
MyApp.currentCall.hangup(prm);
} catch (Exception ex) {
System.Diagnostics.Debug.WriteLine(@"ERROR: ", ex.Message);
}
}
}

private void updateVideoWindow(bool show)
{
if (MyApp.currentCall != null &&
MyApp.currentCall.vidWin != null &&
MyApp.currentCall.vidPrev != null)
{
long windHandle = 0;
VideoWindowHandle vidWH = new VideoWindowHandle();
if (show) {
IntPtr winPtr = ANativeWindow_fromSurface(JNIEnv.Handle,
incomingView.Holder.Surface.Handle);
windHandle = winPtr.ToInt64();
}
vidWH.handle.setWindow(windHandle);
try {
MyApp.currentCall.vidWin.setWindow(vidWH);
} catch (Exception ex) {
System.Diagnostics.Debug.WriteLine(@"ERROR: ", ex.Message);
}
}
}

private void updateCallState(CallInfo ci)
{
String call_state = "";

if (ci == null) {
acceptCallButton.Visibility = ViewStates.Gone;
hangupCallButton.Text = "OK";
statusTxt.Text = "Call disconnected";
return;
}

if (ci.role == pjsip_role_e.PJSIP_ROLE_UAC) {
acceptCallButton.Visibility = ViewStates.Gone;
}

if (ci.state <
pjsip_inv_state.PJSIP_INV_STATE_CONFIRMED)
{
if (ci.role == pjsip_role_e.PJSIP_ROLE_UAS) {
call_state = "Incoming call..";
/* Default button texts are already 'Accept' & 'Reject' */
} else {
hangupCallButton.Text = "Cancel";
call_state = ci.stateText;
}
} else if (ci.state >=
pjsip_inv_state.PJSIP_INV_STATE_CONFIRMED)
{
acceptCallButton.Visibility = ViewStates.Gone;
call_state = ci.stateText;
if (ci.state == pjsip_inv_state.PJSIP_INV_STATE_CONFIRMED) {
hangupCallButton.Text = "Hangup";
} else if (ci.state ==
pjsip_inv_state.PJSIP_INV_STATE_DISCONNECTED)
{
hangupCallButton.Text = "OK";
call_state = "Call disconnected: " + ci.lastReason;
}
if (ci.state == pjsip_inv_state.PJSIP_INV_STATE_CONFIRMED) {
updateVideoWindow(true);
}
}

peerTxt.Text = ci.remoteUri;
statusTxt.Text = call_state;
}

void ISurfaceHolderCallback.SurfaceChanged(ISurfaceHolder holder, Format format, int width, int height)
{
updateVideoWindow(true);
}

void ISurfaceHolderCallback.SurfaceCreated(ISurfaceHolder holder)
{
}

void ISurfaceHolderCallback.SurfaceDestroyed(ISurfaceHolder holder)
{
updateVideoWindow(false);
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using System;

using Android.App;
using Android.Content.PM;
using Android.Runtime;
using Android.OS;
using Java.Lang;
using Android.Hardware.Camera2;
using Android.Content;

namespace pjsua2xamarin.Droid
{
[Activity(Label = "pjsua2xamarin", Icon = "@mipmap/icon", Theme = "@style/MainTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize )]
public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity
{
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);

Xamarin.Essentials.Platform.Init(this, savedInstanceState);
global::Xamarin.Forms.Forms.Init(this, savedInstanceState);

IntPtr class_ref = JNIEnv.FindClass("org/pjsip/PjCameraInfo2");
if (class_ref != null) {
IntPtr method_id = JNIEnv.GetStaticMethodID(class_ref,
"SetCameraManager", "(Landroid/hardware/camera2/CameraManager;)V");

if (method_id != null) {
CameraManager manager = GetSystemService(Context.CameraService) as CameraManager;

JNIEnv.CallStaticVoidMethod(class_ref, method_id, new JValue(manager));

Console.WriteLine("SUCCESS setting cameraManager");
}
}
JavaSystem.LoadLibrary("c++_shared");
JavaSystem.LoadLibrary("pjsua2");

LoadApplication(new App());
}
public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Android.Content.PM.Permission[] grantResults)
{
Xamarin.Essentials.Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults);

base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="com.teluu.pjsua2xamarin">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="30" />
<application android:label="pjsua2xamarin.Android" android:theme="@style/MainTheme"></application>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.CAPTURE_AUDIO_OUTPUT" />
<uses-permission android:name="android.permission.CAPTURE_VIDEO_OUTPUT" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
</manifest>
Loading
Loading