Microsoft Lync and Skype for Business have a rich set of .NET APIs which make it easy to extend the platform and integrate it with other applications. This blog helps explain how to use those APIs.

What are Conversation objects for?

Posted: February 22nd, 2012 | Author: | Filed under: UCMA 2.0, UCMA 3.0 | Tags: | No Comments »

To make it possible for developers to extend the functionality of Lync without having to deal with the nitty-gritty details of Session Initiation Protocol (a.k.a. SIP), UCMA introduces a lot of abstractions. One of the abstractions that pops up most frequently is the Conversation class. Unfortunately, it’s also one of the most confusing to new UCMA developers. This is partly because its purpose isn’t immediately obvious when you start doing things like sending IMs or answering audio calls. Continue reading “What are Conversation objects for?” »


Calling an Exchange UM voicemail box directly

Posted: January 22nd, 2012 | Author: | Filed under: Lync Development, UCMA 2.0, UCMA 3.0 | Tags: , | 4 Comments »

A few posts ago I explained how you can determine whether a call placed by your UCMA application has been answered by an Exchange voicemail box. In this post I want to share another quick tip on UCMA and voicemail: how to place a call directly to a voicemail box. Continue reading “Calling an Exchange UM voicemail box directly” »


Manipulating SIP headers with UCMA

Posted: October 7th, 2011 | Author: | Filed under: UCMA 2.0, UCMA 3.0 | Tags: , | 5 Comments »

For the most part, UCMA keeps Lync operations at a high level, and allows you to ignore the details of the SIP messages that are going back and forth. At times, however, you may need to look at the actual SIP message that a UCMA application is sending or receiving, or add a specific header to an outgoing message. This post shows how to do both of these things. Continue reading “Manipulating SIP headers with UCMA” »


The steps in a Lync transfer

Posted: September 12th, 2011 | Author: | Filed under: UCMA 2.0, UCMA 3.0 | Tags: | No Comments »

The steps in a transfer have always confused me a bit, and since I’ve found that other people also sometimes get confused when working with Lync transfers in UCMA, I thought I would write up a few notes on how they work.

Before I knew much of anything about telephony, I had a vague notion that transfers worked as in the diagram below. Phone A would be in a call with Phone B, and B’s end of the call would sort of get passed over to Phone C.

If you think of transfers this way, you would expect that when you transfer a call, the original call between A and B stays active and becomes a call between A and C. You would also probably expect that Phone A doesn’t need to do anything in order for the transfer to occur, and that all the work happens between Phone B and Phone C as they switch places. Making sense so far?

In actual fact, what happens in Lync is very different. Here are the steps, in a nutshell:

  • Endpoint B sends a REFER message to Endpoint A. This message has the SIP URI of Endpoint C.
  • Endpoint A initiates an entirely new call to Endpoint C.
  • The original call between A and B is terminated.

This last step, terminating the original call, can happen either immediately after the REFER message, or after the call from A to C connects successfully. In Lync, this is the difference between an unattended transfer (the former case) and an attended transfer (the latter case).

Here is a diagram of the actual process:

One case where this can be confusing is if you are looking at the call state changes. Let’s say you’ve hooked up an event handler to the AudioVideoCall.StateChanged event on the call between A and B, to record every state change. This is what you’ll see when you call the BeginTransfer method on that AudioVideoCall object:

  • Established
  • Transferring
  • Terminating
  • Terminated

This often confuses UCMA developers at first, because it appears as though the transfer has failed and the call has terminated. But what’s actually happened here is that the transfer has succeeded, and so the original call (between A and B, in our diagram) can terminate. If neither Endpoint A nor Endpoint C are managed by your UCMA application, you no longer have any control over the new call that results from the transfer, so you can’t track its state changes even if you want to.

There is another special type of transfers, supervised transfers, which I won’t go into here since I’ve covered them in a previous post. I also have a post showing how to perform a transfer in UCMA from back in the 2.0 days (the process hasn’t really changed in UCMA 3.0). Finally, if you want a more comprehensive discussion of transfers and other ways to have fun with audio calls in UCMA, you can always refer to the book.


Invisible OCS conference participants

Posted: February 21st, 2010 | Author: | Filed under: OCS Development, UCMA 2.0 | 1 Comment »

invisible Many of you, since encountering OCS 2007 R2 and its trusty sidekick server-side API, UCMA 2.0, have been wondering, “How do I spy on people and secretly record their audio conferences?”

Luckily for you, to complement its delightfully straightforward automation of SIP messaging, UCMA 2.0 has a rich array of covert operations functionality.

That may be a slight exaggeration. What it does provide is a way for server-side applications to perform a “trusted conference join.” A UCMA 2.0 application running as a trusted service which has authenticated with the OCS server by means of a certificate can join a conference invisibly, meaning it does not show up in the roster of conference participants.

Although I was tempted to title this article “Wiretapping in UCMA 2.0,” the trusted conference join feature has many uses. It allows you to create applications that provide services to OCS conferences without causing distracting and tacky-looking bots to appear in the list of participants. Some examples:

  • Recording conversations for auditing, monitoring, or training purposes
  • Conference timekeeping for businesses that bill by the minute
  • Piping music or audio announcements into a conference
  • Silent monitoring of conferences for training purposes
  • Scaring people in a conference by suddenly saying something when they didn’t know you were there

So how do you do it?

First of all, your application needs to be using an application endpoint rather than a user endpoint. User endpoints cannot join conferences as a trusted participant.

When you call the BeginJoin method on the ConferenceSession object to join a conference,you can supply a ConferenceJoinInformation object with the URI of the conference you want to join. This ConferenceJoinInformation object also has a property called IsTrustedJoin. When this property is set to true,your application endpoint will join the conference as a trusted participant.

Let’s say you are joining a conference that has already started, and you have the conference URI stored in a local variable. You would do something like this:

// Create a ConferenceJoinInformation object with the conference URI
ConferenceJoinInformation joinInfo = new ConferenceJoinInformation(    
    new Microsoft.Rtc.Signaling.RealTimeAddress(conferenceUri));

// Make this a trusted join
joinInfo.IsTrustedJoin = true;

// Create a new conversation 
conversation = new Conversation(_applicationEndpoint);

// Use the conversation to join the conference
conversation.ConferenceSession.BeginJoin(joinInfo,    
    result =>    
    {        
        conversation.ConferenceSession.EndJoin(result);    
    },    
    null);

First, you create a new instance of ConferenceJoinInformation, passing in the conference URI. In order for the constructor to like it, you need to turn the string that contains the URI into a RealTimeAddress object.

Next, you set the IsTrustedJoin property to true.

Finally, you create a new conversation and call the BeginJoin method, passing in the join information.

When the asynchronous operation completes, your application will be a participant in the conference, sending and receiving media like any other, but it will be INVISIBLE.

There is one other point I want to call out here before concluding. If you want to have more than one of these invisible participants from the same application (e.g. one to record and one to make animal noises) you can do this, on two conditions: you will need to create a Conversation object for each participant, and you will need to impersonate a fake URI so that the participants have different URIs.

To do this, you use the Impersonate method on the Conversation object, as below:

// Create a new conversation with impersonation
conversation = new Conversation(_applicationEndpoint);
conversation.Impersonate("RandomSounds@____________.com", 
    "tel:+15555551212", "Random Sounds");

The URI you use can be completely fabricated; it doesn’t need to be a real contact.

This technique can be especially handy when combined with a back-to-back user agent (B2BUA) to proxy remote users into a conference invisibly. More on this in a future post if there is interest.

I take no responsibility for any imprudent or illegal things you do with trusted conference participants!


BeginDialOut with Office Communicator Clients

Posted: October 8th, 2009 | Author: | Filed under: OCS Development, UCMA 2.0 | No Comments »

Recently I was working on some code to invite new participants to an A/V conference, and learned about some Office Communicator behavior that may throw you off if you are trying to dial out to a URI.

The AudioVideoMcuSession class exposes a BeginDialOut method which tells the MCU to dial out to a particular URI to bring in a new participant. By supplying McuDialOutOptions, you can specify the display name and language to use for the invited participant, which can be useful if you are inviting someone at a PSTN phone number.

If your invitation goes to an endpoint running Office Communicator, though, Communicator will reject the dial-out invitation, take the conference URI, and join the conference itself. It’s like those bosses you hear horror stories about who will brashly reject a new idea you propose, and then suggest it themselves at the next company meeting.

So if you call BeginDialOut with a SIP URI and the endpoint that receives the dial-out is a Communicator client, you will see a ConferenceFailureException in your code with the reason “userDeclined,” but Communicator will still successfully join the conference.

You have two options for dealing with this: you can ignore that particular exception, confirm that the invited participant has joined the conference,and proceed; or you can use the BeginInviteRemoteParticipants method on the Conversation object to bring in the new participants. If all you have is the AudioVideoMcuSession,you can find the associated Conversation at AudioVideoMcuSession.ConferenceSession.Conversation.


Accepting transfers and forwards in UCMA 2.0 applications

Posted: July 15th, 2009 | Author: | Filed under: OCS Development, UCMA 2.0 | No Comments »
scrap
what happens if you let UCMA clean your house

The layer of abstraction that makes UCMA 2.0 such a powerful tool can also, once in a while, be dangerous. If we’re not careful enough, we can easily get the idea that the API will take care of everything for us: SIP messaging, media negotiation, presence subscriptions, house cleaning. UCMA does handle the connection to Office Communications Server without being asked (and it cooks a delicious filet mignon), but there are some things you need to remember to explicitly tell it to do.

My previous post on the UseRegistration property describes one of these. Another one that has tripped me up a couple of times is accepting transfers. If you’ve used the transfer methods in UCMA, you know how easy it is to send a transfer to a remote endpoint, with a single asynchronous method call and a handful of parameters. If the remote endpoint is Office Communicator, the new conversation window pops up and your transfer goes off without a hitch. What happens, though,if your UCMA application receives a transfer?

In other words,what happens if you call into your UCMA application from Communicator, hit the transfer button, and transfer the call to another number?

The answer is that, by default, nothing happens. The transfer fails, because UCMA 2.0 applications don’t accept transfers out of the box.

To allow your application to receive transfers for a call, you need to handle the TransferReceived event on that call:

_avCall.TransferReceived += 
    new EventHandler(_avCall_TransferReceived);

The code to accept the transfer, in the grand tradition of UCMA 2.0, consists of one method called Accept, with a parameter (signaling headers) that can almost always be null.

void _avCall_TransferReceived(object sender, 
    AudioVideoCallTransferReceivedEventArgs e)
{
    e.Accept(null);
}

Well, there you have it. With that minor alteration, your application now accepts transfers, at least on that one AudioVideoCall.

If you want to accept forwards (really only applicable for outgoing calls) you’re looking at something very similar:

_avCall.Forwarded += 
    new EventHandler(_avCall_Forwarded);

The code to accept a forward is even simpler:

void _avCall_Forwarded(object sender, CallForwardReceivedEventArgs e)
{
    e.Accept();
}

If that isn’t cool enough for you (I admit it’s a little anticlimactic), you can be more selective about the transfers you accept.

void _avCall_TransferReceived(object sender, 
    AudioVideoCallTransferReceivedEventArgs e)
{
    if (e.TransferredBy == "sip:telemarketer@useless-widget.com")
    {
        e.Decline();
    }
    else
    {
        e.Accept(null);
    }
}

Finally, depending on what you are doing with your application, you will probably want a handle to the new Conversation initiated by the transfer:

void _avCall_TransferReceived(object sender, 
    AudioVideoCallTransferReceivedEventArgs e)
{
    e.Accept(null);
 
    _conversation = e.NewConversation;
}

As you can see, this is all still very simple. Just remember to do it whenever your application needs to accept transfers, and you’re in business.


Escalating an AudioVideoCall to a conference in UCMA 2.0

Posted: May 8th, 2009 | Author: | Filed under: OCS Development, UCMA 2.0 | 4 Comments »

If you’ve ever tried to escalate an AudioVideoCall to a conference, you will know that it isn’t the smooth, carefree experience that the words “conference escalation” call to mind. It has its pitfalls. Take a look at the following code:

private void CreateCallAndEscalate()
{
     // Create a conversation and a call.
     _conversation = new Conversation(_endpoint);
     _call = new AudioVideoCall(_conversation);
     _call.BeginEstablish(_yourSipUri, null, CallEstablishCompleted, null);
}

private void CallEstablishCompleted(IAsyncResult result)
{
     _call.EndEstablish(result);
     _conversation.ConferenceSession.BeginJoin(JoinCompleted, null);
}

private void JoinCompleted(IAsyncResult result)
{
     _conversation.ConferenceSession.EndJoin(result);

     try
     {
          _conversation.BeginEscalateToConference(EscalateCompleted, null);
     }
     catch (InvalidOperationException ex)
     {
          Console.WriteLine("Escalate failed:");
          Console.WriteLine(ex.ToString());
     }
}

private void EscalateCompleted(IAsyncResult result)
{
     try
     {
          _conversation.EndEscalateToConference(result);
     }
     catch (RealTimeException ex)
     {
          Console.WriteLine("Escalate failed:");
          Console.WriteLine(ex.ToString());
     }
}

At first glance, you might expect this code to cheerfully bump your AudioVideoCall up into the ad hoc conference and frolic off into the sunset. But no such luck. Run it, and you will be furnished with the following exception message:

Microsoft.Rtc.Signaling.OperationFailureException: The EscalateToConferenceAsyncResult operation has failed with message: “Call cannot escalate to conference, mediaProvider does not support escalation”. See the InnerException and FailureReason properties as well as the logs for additional information.  —> System.InvalidOperationException: Call cannot escalate to conference, mediaProvider does not support escalation  at Microsoft.Rtc.Collaboration.Call.BeginEscalate(McuSession mcuSession,AsyncCallback userCallback,Object state)

To make a long story short, the media provider for AudioVideoCalls doesn’t have built-in support for escalation to a conference. Thankfully, there IS a way to get an existing two-party call into an ad hoc conference. The ConferenceSession object that belongs to the Conversation in turn has its own AudioVideoMcuSession. You can call the BeginTransfer method on the AudioVideoMcuSession to transfer an existing two-party call to the MCU, effectively bringing it into the conference. The new JoinCompleted method would look something like this:

private void JoinCompleted(IAsyncResult result)
{
     _conversation.ConferenceSession.EndJoin(result);
     try
     {
          _conversation.ConferenceSession.AudioVideoMcuSession.BeginTransfer(
               _call, null, McuTransferCompleted, null);
     }
     catch (InvalidOperationException ex)
     {
          Console.WriteLine("MCU transfer failed:");
          Console.WriteLine(ex.ToString());
     }
}

You also need a callback method:

private void McuTransferCompleted(IAsyncResult result)
{
     try
     {
          _conversation.ConferenceSession.AudioVideoMcuSession.EndTransfer(
              result);
     }
     catch (RealTimeException ex)
     {
          Console.WriteLine("MCU transfer failed:");
          Console.WriteLine(ex.ToString());
     }
}

When you run this code (you’ll need to do the extra work of setting up a CollaborationPlatform and endpoint and so forth) it will look very much like the application is placing a call and then escalating it to a conference. For a lot of situations (such as accepting incoming two-party calls and then bringing them into a conference) this will fit the bill perfectly well.

Feel free to email me if you have questions about this or if you’d like the code for the full sample application.


Transferring calls in UCMA 2.0, part one

Posted: April 14th, 2009 | Author: | Filed under: OCS Development, UCMA 2.0 | No Comments »

College students, like very small children, are easily entertained. Back in my college days, I always thought it would be fun to play with the campus PBX phone system by calling a classmate and then transferring the call to a random professor as soon as they answered the phone. The ensuing call would go something like this:

Student: Hello?image

<>

Professor: Hello?

Student: Hello?

Professor: Yes, this is Professor Wolford; who is this?

Student: I think you may have the wrong number…

Professor: Did you call me?

Student: I don’t think so; maybe it was someone else?

Professor: Then why are you on the phone? I’m not sure I understand.

Student: This is my phone. You called 4512, right?

Professor: You called me. At least, my phone rang.

Student: I’m sorry, I thought my phone rang…

Professor: Excuse me, I have a good deal of work to do. Please don’t call me again. Thank you.

<>

Now, if only I’d had UCMA 2.0 back in my college years, I could have pulled this off with an Office Communication Server deployment and a few well-chosen lines of code. I wouldn’t even have had to mess with pulsating dial tones and FLASH buttons.

There are many more practical uses for code-driven transferring of calls, and I’m going to show you a few ways of doing this using UCMA 2.0. If you use this knowledge to set up stupid phone pranks instead of creating an auto-dialer for your call center or placing calls on behalf of employees through your CRM system,I take no responsibility for the results.

As our starting point,we’ll use the template I gave you a few posts ago for the PresenceMonitor class. To get things off the ground, go ahead and change the SubscribeToPresence method to ExecuteTransfer.

private void EstablishCompleted(IAsyncResult result)
{
    _endpoint.EndEstablish(result);
    Console.WriteLine("Established endpoint.");

    ExecuteTransfer();
}

We’re also going to need a handful of new instance variables, so I’ll just give them to you now to paste somewhere around the top of your class. Leave the constants with server settings alone, and make sure you have the following for your private instance variables:

private string _mySipUri;
private string _yourSipUri;
private string _yourSipUri2;
private CollaborationPlatform _platform;
private UserEndpoint _endpoint;
private Conversation _conversation;
private AudioVideoCall _call;
private Conversation _conversation2;
private AudioVideoCall _call2;

Also, change the first part of the run method, where it collects SIP URIs, to take three instead of two:

Console.Write("Enter your SIP URI: ");
_mySipUri = Console.ReadLine();

Console.Write("Enter another SIP URI: ");
_yourSipUri = Console.ReadLine();

Console.Write("Enter a third SIP URI: ");
_yourSipUri2 = Console.ReadLine();

To kick things off, paste the following into your class for the ExecuteTransfer method. All we’ll do for now is create a new Conversation object and a new AudioVideoCall object associated with that conversation.

private void ExecuteTransfer()
{
    // Create a conversation and a call.
    _conversation = new Conversation(_endpoint);
    _call = new AudioVideoCall(_conversation);
}

At the bottom of the method, add another line of code to actually place the call. This particular override of the BeginEstablish method allows us to specify a single SIP URI with which we want to establish a one-on-one call (or peer-to-peer call, as we say in the biz).

_call.BeginEstablish(_yourSipUri, null, CallEstablishCompleted, null);

As you can see, we’ll need a callback method named CallEstablishCompleted. If you move really quickly, you may be able to paste it in before Visual Studio starts getting antsy and marking things with squiggly red lines. Hurry up!

private void CallEstablishCompleted(IAsyncResult result)
{
    _call.EndEstablish(result);

    // Execute an unattended transfer
    _call.BeginTransfer(_yourSipUri2, 
        new CallTransferOptions(CallTransferType.Unattended),
        TransferCompleted, null);
}

The first parameter of BeginTransfer is the SIP URI of the transfer target – the SIP URI to which we want to transfer the call. The second parameter is an instance of CallTransferOptions, which in turn takes a CallTransferType, in this case Unattended. The third and fourth parameters are the usual suspects, our callback delegate and state object.

We’re starting with the unattended transfer type because in a way it’s the most primitive. In an unattended transfer, Endpoint A says to Endpoint B, “Go talk to this other person,” and then hangs up without waiting to see that the transfer goes through. Endpoint A has no way of knowing whether the transfer was successful. Unattended transfer is for the risk-takers and daredevils among endpoints.

Last but not least, we need a callback method for the transfer operation. This one is straightforward.

private void TransferCompleted(IAsyncResult result)
{
    try
    {
        _call.EndTransfer(result);
    }
    catch (OperationFailureException ex)
    {
        Console.WriteLine("Couldn't complete the transfer.");
    }
    Console.WriteLine("Transfer initiated.");
}

Once you’ve plugged in this last callback method, go ahead and test out the code. (You’ll need to have a console application or something like that to start everything up; I’ll let you take care of that on your own.)

A couple of friendly reminders, since I always forget these myself: first, make sure to add sip: to the beginning of the SIP URIs you enter when testing, or things will break, and there won’t even be any exciting flying sparks. Second, if you have any weird issues, take a close look at the SIP URIs you entered. I can’t count the number of times I’ve spent a solid hour on fruitless debugging only to realize that, thanks to a misspelled configuration setting, my code was trying to contact someone at Clartiy Consulting instead of Clarity.

If all goes well, when you run the sample you will get an incoming call from your own SIP URI at the second SIP URI you entered. As soon as you answer the call, a new call window will pop up with an outgoing call to the third SIP URI, and the first call will hang up.

So far, so good.

Let’s try a subtle but important change. Go back to the CallEstablishCompleted method and change the CallTransferType to CallTransferType.Attended.

_call.BeginTransfer(_yourSipUri2, 
    new CallTransferOptions(CallTransferType.Attended),
    TransferCompleted, null);

Now run your program again and watch carefully what happens.

This time, when that second call window pops up with the transfer, the initial call will stay put while the transfer goes through, looking something like this:

image

It won’t hang up until the person at the third SIP URI answers the transferred call. It’s sort of like when you give someone a ride home, and you wait until they’ve gotten in the front door and waved goodbye before you drive away, just in case they forgot their keys and no one’s home.

If you don’t answer the transferred call, the EndTransfer method will throw an OperationFailureException with a message that the transfer couldn’t be completed, which we’re catching in the TransferCompleted method. At this point, you’ll be able to resume the first call if you like.

If you’ve even been on hold with a customer service agent, and they’ve said to you, “I’m going to see if so-and-so is available, and if she is, then I’ll transfer you; if not, I’ll be back,” then you’ve seen this process at work. With an attended transfer, your code can keep tabs on whether the transfer worked, and resume the initial call if it didn’t.

Next, we’re going to look at what is probably the coolest of the transfer types: the supervised transfer. It only works with OCS 2007 R2, and the clients you are calling must be using Office Communicator 2007 R2 for the transfer to go through properly. The supervised transfer involves something called call replacing.

The one issue we have with the transfers we’ve done so far is that we have to place the first call and then wait until the person answers before transferring them. This puts a bit of a damper on the prank – or, er, the call center auto-dialer. It would be nice if we could place both calls at once, and just transfer one into the other when we’ve got them both on the line.

So that’s exactly what we’re going to do.

Go back to your ExecuteTransfer method and change it so that it creates two conversations (_conversation and _conversation2) and two calls (_call and _call2). I’m sure you can do this yourself, but there’s an extra wrinkle we need to take care of, so here’s the code:

private void ExecuteTransfer()
{
    // Create a conversation and a call.
    _conversation = new Conversation(_endpoint);
    _call = new AudioVideoCall(_conversation);
    _conversation2 = new Conversation(_endpoint);
    _call2 = new AudioVideoCall(_conversation2);

    // Establish a call to the first SIP URI.
    _call.BeginEstablish(_yourSipUri, null, CallEstablishCompleted, _call);
    _call2.BeginEstablish(_yourSipUri2, null, 
        CallEstablishCompleted, _call2);
}

The important thing to note is that we’re passing the call we’re establishing into the BeginEstablish method as the state parameter. This will allow us to magically pull it out of the IAsyncResult in a moment. Stay tuned.

Here’s our brand new CallEstablishCompleted method. This one works a little differently, since it needs to deal with callbacks for two different calls. Notice how we’re getting the correct AudioVideoCall object from the AsyncState property on the IAsyncResult, so we can call EndEstablish on the right call.

private void CallEstablishCompleted(IAsyncResult result)
{
    AudioVideoCall call = result.AsyncState as AudioVideoCall;
    call.EndEstablish(result);

    if (_call.State == CallState.Established && 
        _call2.State == CallState.Established)
    {
        // Execute a supervised transfer
        _call.BeginTransfer(_call2, TransferCompleted, null);
    }
}

We also check to see if both calls are established; once we’ve got both of them up and running, we begin the transfer. We’re using a new and unfamiliar override of BeginTransfer here – this one takes three parameters, and the first one is another AudioVideoCall object. What’s going on here?

This supervised transfer works by adding a Replaces header to the SIP REFER message that tells the other endpoint to go talk to someone else. The Replaces header tells the endpoint to plug into a specific existing call, so that we can seamlessly bridge together two calls we’ve already established separately with different people.

Run the sample and see what happens. This time, you won’t see two different call windows when the transfer is in process. Instead, the call will magically turn into a call with a different person.

You now have several new tools at your disposal – unattended transfer, attended transfer, and supervised transfer – that may be used either for good or for evil. I trust you to make the right decisions.

The finished class, in all its glory, is here.

Check back soon for installment two, in which I delve into some even crazier territory with dialing out from conferences and a roundabout way to escalate AudioVideoCalls.


Troubleshooting TLSException in UCMA 2.0 applications

Posted: March 25th, 2009 | Author: | Filed under: OCS Development, UCMA 2.0 | No Comments »

One of the most common issues you are likely to run into in doing UCMA 2.0 development is the following exception:

Microsoft.Rtc.Internal.Sip.TLSException: CertificateInfoNative::AcquireCredentialsHandle() failed; HRESULT=-2146893043

Most often, it means that the process does not have permission to access the certificate you are using to authenticate for transport layer security (TLS). There are a number of reasons why this might happen.

If you are debugging your application in Visual Studio, make sure you are running Visual Studio as an administrator. You can do this by right-clicking on Visual Studio in the start menu and choosing Run as administrator.

If it’s a console application or a Windows service you’re trying to run, make sure the account you are running it under has permission to access the private key of your certificate.

For websites running in IIS, you may need to use WinHttpCertCfg.exe to grant access to the private key.