/// <summary> /// Places an outgoing SIP call. /// </summary> /// <param name="destination">The SIP URI to place a call to. The destination can be a full SIP URI in which case the all will /// be placed anonymously directly to that URI. Alternatively it can be just the user portion of a URI in which case it will /// be sent to the configured SIP server.</param> public async Task Call(string destination) { // Determine if this is a direct anonymous call or whether it should be placed using the pre-configured SIP server account. SIPURI callURI = null; string sipUsername = null; string sipPassword = null; string fromHeader = null; if (destination.Contains("@") || m_sipServer == null) { // Anonymous call direct to SIP server specified in the URI. callURI = SIPURI.ParseSIPURIRelaxed(destination); fromHeader = (new SIPFromHeader(m_sipFromName, SIPURI.ParseSIPURI(SIPFromHeader.DEFAULT_FROM_URI), null)).ToString(); } else { // This call will use the pre-configured SIP account. callURI = SIPURI.ParseSIPURIRelaxed(destination + "@" + m_sipServer); sipUsername = m_sipUsername; sipPassword = m_sipPassword; fromHeader = (new SIPFromHeader(m_sipFromName, new SIPURI(m_sipUsername, m_sipServer, null), null)).ToString(); } StatusMessage(this, $"Starting call to {callURI}."); var lookupResult = await Task.Run(() => { return(SIPDNSManager.ResolveSIPService(callURI, false)); }); if (lookupResult == null || lookupResult.LookupError != null) { StatusMessage(this, $"Call failed, could not resolve {callURI}."); } else { var dstEndpoint = lookupResult.GetSIPEndPoint(); StatusMessage(this, $"Call progressing, resolved {callURI} to {dstEndpoint}."); System.Diagnostics.Debug.WriteLine($"DNS lookup result for {callURI}: {dstEndpoint}."); SIPCallDescriptor callDescriptor = new SIPCallDescriptor(sipUsername, sipPassword, callURI.ToString(), fromHeader, null, null, null, null, SIPCallDirection.Out, _sdpMimeContentType, null, null); var audioSrcOpts = new AudioOptions { AudioSource = AudioSourcesEnum.Microphone, OutputDeviceIndex = m_audioOutDeviceIndex }; var videoSrcOpts = new VideoOptions { VideoSource = VideoSourcesEnum.TestPattern, SourceFile = RtpAVSession.VIDEO_TESTPATTERN, SourceFramesPerSecond = VIDEO_LIVE_FRAMES_PER_SECOND }; MediaSession = new RtpAVSession(audioSrcOpts, videoSrcOpts); m_userAgent.RemotePutOnHold += OnRemotePutOnHold; m_userAgent.RemoteTookOffHold += OnRemoteTookOffHold; await m_userAgent.InitiateCallAsync(callDescriptor, MediaSession); } }
static async Task Main() { Console.WriteLine("SIPSorcery Getting Started Demo"); AddConsoleLogger(); var sipTransport = new SIPTransport(); var userAgent = new SIPUserAgent(sipTransport, null); var rtpSession = new RtpAVSession(new AudioOptions { AudioSource = AudioSourcesEnum.Microphone }, null); // Place the call and wait for the result. bool callResult = await userAgent.Call(DESTINATION, null, null, rtpSession); Console.WriteLine($"Call result {((callResult) ? "success" : "failure")}."); Console.WriteLine("press any key to exit..."); Console.Read(); if (userAgent.IsCallActive) { Console.WriteLine("Hanging up."); userAgent.Hangup(); } // Clean up. sipTransport.Shutdown(); SIPSorcery.Net.DNSManager.Stop(); }
/// <summary> /// Because this is a server user agent the SIP transport must start listening for client user agents. /// </summary> private static async Task OnRequest(SIPEndPoint localSIPEndPoint, SIPEndPoint remoteEndPoint, SIPRequest sipRequest) { try { if (sipRequest.Header.From != null && sipRequest.Header.From.FromTag != null && sipRequest.Header.To != null && sipRequest.Header.To.ToTag != null) { // This is an in-dialog request that will be handled directly by a user agent instance. } else if (sipRequest.Method == SIPMethodsEnum.INVITE) { Log.LogInformation($"Incoming call request: {localSIPEndPoint}<-{remoteEndPoint} {sipRequest.URI}."); var userAgent = new SIPUserAgent(_sipTransport, null); userAgent.ServerCallCancelled += (uas) => Log.LogDebug("Incoming call cancelled by remote party."); userAgent.OnCallHungup += (dialog) => _waveFile?.Close(); var rtpSession = new RtpAVSession( new AudioOptions { AudioSource = AudioSourcesEnum.CaptureDevice, AudioCodecs = new List <SDPMediaFormatsEnum> { SDPMediaFormatsEnum.PCMU, SDPMediaFormatsEnum.PCMA } }, null); rtpSession.OnRtpPacketReceived += OnRtpPacketReceived; var uas = userAgent.AcceptCall(sipRequest); await userAgent.Answer(uas, rtpSession); if (userAgent.IsCallActive) { await rtpSession.Start(); } } else if (sipRequest.Method == SIPMethodsEnum.BYE) { SIPResponse byeResponse = SIPResponse.GetResponse(sipRequest, SIPResponseStatusCodesEnum.CallLegTransactionDoesNotExist, null); await _sipTransport.SendResponseAsync(byeResponse); } else if (sipRequest.Method == SIPMethodsEnum.SUBSCRIBE) { SIPResponse notAllowededResponse = SIPResponse.GetResponse(sipRequest, SIPResponseStatusCodesEnum.MethodNotAllowed, null); await _sipTransport.SendResponseAsync(notAllowededResponse); } else if (sipRequest.Method == SIPMethodsEnum.OPTIONS || sipRequest.Method == SIPMethodsEnum.REGISTER) { SIPResponse optionsResponse = SIPResponse.GetResponse(sipRequest, SIPResponseStatusCodesEnum.Ok, null); await _sipTransport.SendResponseAsync(optionsResponse); } } catch (Exception reqExcp) { Log.LogWarning($"Exception handling {sipRequest.Method}. {reqExcp.Message}"); } }
/// <summary> /// Answers an incoming SIP call. /// </summary> public async Task <bool> Answer() { if (m_pendingIncomingCall == null) { StatusMessage(this, $"There was no pending call available to answer."); return(false); } else { var sipRequest = m_pendingIncomingCall.ClientTransaction.TransactionRequest; SDP offerSDP = SDP.ParseSDPDescription(sipRequest.Body); bool hasAudio = offerSDP.Media.Any(x => x.Media == SDPMediaTypesEnum.audio && x.MediaStreamStatus != MediaStreamStatusEnum.Inactive); bool hasVideo = offerSDP.Media.Any(x => x.Media == SDPMediaTypesEnum.video && x.MediaStreamStatus != MediaStreamStatusEnum.Inactive); AudioOptions audioOpts = new AudioOptions { AudioSource = AudioSourcesEnum.None }; if (hasAudio) { audioOpts = new AudioOptions { AudioSource = AudioSourcesEnum.CaptureDevice, OutputDeviceIndex = m_audioOutDeviceIndex, AudioCodecs = new List <SDPMediaFormatsEnum> { SDPMediaFormatsEnum.PCMU, SDPMediaFormatsEnum.PCMA } }; } VideoOptions videoOpts = new VideoOptions { VideoSource = VideoSourcesEnum.None }; if (hasVideo) { videoOpts = new VideoOptions { VideoSource = VideoSourcesEnum.TestPattern, SourceFile = RtpAVSession.VIDEO_TESTPATTERN, SourceFramesPerSecond = VIDEO_LIVE_FRAMES_PER_SECOND }; } MediaSession = new RtpAVSession(audioOpts, videoOpts); m_userAgent.RemotePutOnHold += OnRemotePutOnHold; m_userAgent.RemoteTookOffHold += OnRemoteTookOffHold; bool result = await m_userAgent.Answer(m_pendingIncomingCall, MediaSession); m_pendingIncomingCall = null; return(result); } }
static async Task DialNumber() { string fromHeader = (new SIPFromHeader(USERNAME, new SIPURI(USERNAME, DOMAIN, null), null)).ToString(); SIPCallDescriptor callDescriptor = new SIPCallDescriptor(USERNAME, PASSWORD, DEFAULT_CALL_DESTINATION, fromHeader, null, null, null, null, SIPCallDirection.Out, SDP.SDP_MIME_CONTENTTYPE, null, null); callDescriptor.CallId = "12028883999"; userAgent = new SIPUserAgent(sipTransport, null); userAgent.ClientCallTrying += (uac, resp) => Console.WriteLine($"{uac.CallDescriptor.To} Trying: {resp.StatusCode} {resp.ReasonPhrase}."); userAgent.ClientCallRinging += (uac, resp) => Console.WriteLine($"{uac.CallDescriptor.To} Ringing: {resp.StatusCode} {resp.ReasonPhrase}."); userAgent.ClientCallFailed += (uac, err, resp) => Console.WriteLine($"{uac.CallDescriptor.To} Failed: {err}, Status code: {resp?.StatusCode}"); userAgent.ClientCallAnswered += (uac, resp) => Console.WriteLine($"{uac.CallDescriptor.To} Answered: {resp.StatusCode} {resp.ReasonPhrase}."); userAgent.OnDtmfTone += (key, duration) => OnDtmfTone(userAgent, key, duration); userAgent.OnRtpEvent += (evt, hdr) => Console.WriteLine($"rtp event {evt.EventID}, duration {evt.Duration}, end of event {evt.EndOfEvent}, timestamp {hdr.Timestamp}, marker {hdr.MarkerBit}."); userAgent.OnCallHungup += OnHangup; var rtpSession = new RtpAVSession( new AudioOptions { AudioSource = AudioSourcesEnum.CaptureDevice, AudioCodecs = new List <SDPMediaFormatsEnum> { SDPMediaFormatsEnum.PCMU, SDPMediaFormatsEnum.PCMA } }, null); rtpSession.OnRtpPacketReceived += OnRtpPacketReceived; var callResult = await userAgent.Call(callDescriptor, rtpSession); Console.WriteLine($"Call result {((callResult) ? "success" : "failure")}."); if (callResult) { Console.WriteLine("Enter digits one after another"); for (int i = 0; i < 11; i++) { var p = Console.ReadLine(); await userAgent.SendDtmf(byte.Parse(p)); } } Console.WriteLine("Enter ?"); Console.ReadLine(); await userAgent.SendDtmf(35); Thread.Sleep(60000); userAgent.Hangup(); _waveFile.Dispose(); Console.WriteLine("Hangup"); }
/// <summary> /// Answers an incoming SIP call. /// </summary> public async Task Answer() { if (m_pendingIncomingCall == null) { StatusMessage(this, $"There was no pending call available to answer."); } else { var sipRequest = m_pendingIncomingCall.ClientTransaction.TransactionRequest; SDP offerSDP = SDP.ParseSDPDescription(sipRequest.Body); bool hasAudio = offerSDP.Media.Any(x => x.Media == SDPMediaTypesEnum.audio); bool hasVideo = offerSDP.Media.Any(x => x.Media == SDPMediaTypesEnum.video); AudioOptions audioOpts = new AudioOptions { AudioSource = AudioSourcesEnum.None }; if (hasAudio) { audioOpts = new AudioOptions { AudioSource = AudioSourcesEnum.Microphone }; } VideoOptions videoOpts = new VideoOptions { VideoSource = VideoSourcesEnum.None }; if (hasVideo) { videoOpts = new VideoOptions { VideoSource = VideoSourcesEnum.TestPattern, SourceFile = RtpAVSession.VIDEO_TESTPATTERN, SourceFramesPerSecond = VIDEO_LIVE_FRAMES_PER_SECOND }; } MediaSession = new RtpAVSession(sipRequest.RemoteSIPEndPoint.Address.AddressFamily, audioOpts, videoOpts); m_userAgent.RemotePutOnHold += OnRemotePutOnHold; m_userAgent.RemoteTookOffHold += OnRemoteTookOffHold; await m_userAgent.Answer(m_pendingIncomingCall, MediaSession); m_pendingIncomingCall = null; } }
static async Task Main() { Console.WriteLine("SIPSorcery Getting Started Demo"); AddConsoleLogger(); _waveFile = new WaveFileWriter("output.mp3", _waveFormat); var sipTransport = new SIPTransport(); var userAgent = new SIPUserAgent(sipTransport, null); userAgent.OnCallHungup += (dialog) => _waveFile?.Close(); var rtpSession = new RtpAVSession( new AudioOptions { AudioSource = AudioSourcesEnum.Microphone, AudioCodecs = new List <SDPMediaFormatsEnum> { SDPMediaFormatsEnum.PCMU, SDPMediaFormatsEnum.PCMA } }, null); rtpSession.OnRtpPacketReceived += OnRtpPacketReceived; // Place the call and wait for the result. bool callResult = await userAgent.Call(DESTINATION, null, null, rtpSession); Console.WriteLine($"Call result {((callResult) ? "success" : "failure")}."); Console.WriteLine("press any key to exit..."); Console.Read(); if (userAgent.IsCallActive) { Console.WriteLine("Hanging up."); userAgent.Hangup(); } // Clean up. sipTransport.Shutdown(); SIPSorcery.Net.DNSManager.Stop(); }
static void Main(string[] args) { Console.WriteLine("SIPSorcery user agent server example."); Console.WriteLine("Press h to hangup a call or ctrl-c to exit."); EnableConsoleLogger(); IPAddress listenAddress = IPAddress.Any; IPAddress listenIPv6Address = IPAddress.IPv6Any; if (args != null && args.Length > 0) { if (!IPAddress.TryParse(args[0], out var customListenAddress)) { Log.LogWarning($"Command line argument could not be parsed as an IP address \"{args[0]}\""); listenAddress = IPAddress.Any; } else { if (customListenAddress.AddressFamily == AddressFamily.InterNetwork) { listenAddress = customListenAddress; } if (customListenAddress.AddressFamily == AddressFamily.InterNetworkV6) { listenIPv6Address = customListenAddress; } } } // Set up a default SIP transport. var sipTransport = new SIPTransport(); var localhostCertificate = new X509Certificate2("localhost.pfx"); // IPv4 channels. sipTransport.AddSIPChannel(new SIPUDPChannel(new IPEndPoint(listenAddress, SIP_LISTEN_PORT))); sipTransport.AddSIPChannel(new SIPTCPChannel(new IPEndPoint(listenAddress, SIP_LISTEN_PORT))); sipTransport.AddSIPChannel(new SIPTLSChannel(localhostCertificate, new IPEndPoint(listenAddress, SIPS_LISTEN_PORT))); //sipTransport.AddSIPChannel(new SIPWebSocketChannel(IPAddress.Any, SIP_WEBSOCKET_LISTEN_PORT)); //sipTransport.AddSIPChannel(new SIPWebSocketChannel(IPAddress.Any, SIP_SECURE_WEBSOCKET_LISTEN_PORT, localhostCertificate)); // IPv6 channels. sipTransport.AddSIPChannel(new SIPUDPChannel(new IPEndPoint(listenIPv6Address, SIP_LISTEN_PORT))); sipTransport.AddSIPChannel(new SIPTCPChannel(new IPEndPoint(listenIPv6Address, SIP_LISTEN_PORT))); sipTransport.AddSIPChannel(new SIPTLSChannel(localhostCertificate, new IPEndPoint(listenIPv6Address, SIPS_LISTEN_PORT))); //sipTransport.AddSIPChannel(new SIPWebSocketChannel(IPAddress.IPv6Any, SIP_WEBSOCKET_LISTEN_PORT)); //sipTransport.AddSIPChannel(new SIPWebSocketChannel(IPAddress.IPv6Any, SIP_SECURE_WEBSOCKET_LISTEN_PORT, localhostCertificate)); EnableTraceLogs(sipTransport); string executableDir = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location); // To keep things a bit simpler this example only supports a single call at a time and the SIP server user agent // acts as a singleton SIPServerUserAgent uas = null; CancellationTokenSource rtpCts = null; // Cancellation token to stop the RTP stream. RtpAVSession rtpSession = null; // Because this is a server user agent the SIP transport must start listening for client user agents. sipTransport.SIPTransportRequestReceived += async(SIPEndPoint localSIPEndPoint, SIPEndPoint remoteEndPoint, SIPRequest sipRequest) => { try { if (sipRequest.Method == SIPMethodsEnum.INVITE) { SIPSorcery.Sys.Log.Logger.LogInformation($"Incoming call request: {localSIPEndPoint}<-{remoteEndPoint} {sipRequest.URI}."); // Check there's a codec we support in the INVITE offer. var offerSdp = SDP.ParseSDPDescription(sipRequest.Body); IPEndPoint dstRtpEndPoint = SDP.GetSDPRTPEndPoint(sipRequest.Body); if (offerSdp.Media.Any(x => x.Media == SDPMediaTypesEnum.audio && x.HasMediaFormat((int)SDPMediaFormatsEnum.PCMU))) { Log.LogDebug($"Client offer contained PCMU audio codec."); rtpSession = new RtpAVSession( new AudioOptions { AudioSource = AudioSourcesEnum.Music, SourceFile = executableDir + "/" + AUDIO_FILE_PCMU }, null); rtpSession.setRemoteDescription(new RTCSessionDescription { type = RTCSdpType.offer, sdp = offerSdp }); } if (rtpSession == null) { // Didn't get a match on the codecs we support. SIPResponse noMatchingCodecResponse = SIPResponse.GetResponse(sipRequest, SIPResponseStatusCodesEnum.NotAcceptableHere, null); await sipTransport.SendResponseAsync(noMatchingCodecResponse); } else { // If there's already a call in progress hang it up. Of course this is not ideal for a real softphone or server but it // means this example can be kept simpler. if (uas?.IsHungup == false) { uas?.Hangup(false); } rtpCts?.Cancel(); rtpCts = new CancellationTokenSource(); UASInviteTransaction uasTransaction = new UASInviteTransaction(sipTransport, sipRequest, null); uas = new SIPServerUserAgent(sipTransport, null, null, null, SIPCallDirection.In, null, null, null, uasTransaction); uas.CallCancelled += (uasAgent) => { rtpCts?.Cancel(); rtpSession.CloseSession(null); }; rtpSession.OnRtpClosed += (reason) => uas?.Hangup(false); uas.Progress(SIPResponseStatusCodesEnum.Trying, null, null, null, null); uas.Progress(SIPResponseStatusCodesEnum.Ringing, null, null, null, null); var answerSdp = await rtpSession.createAnswer(null); uas.Answer(SDP.SDP_MIME_CONTENTTYPE, answerSdp.ToString(), null, SIPDialogueTransferModesEnum.NotAllowed); await rtpSession.Start(); } } else if (sipRequest.Method == SIPMethodsEnum.BYE) { SIPSorcery.Sys.Log.Logger.LogInformation("Call hungup."); SIPResponse byeResponse = SIPResponse.GetResponse(sipRequest, SIPResponseStatusCodesEnum.Ok, null); await sipTransport.SendResponseAsync(byeResponse); uas?.Hangup(true); rtpSession?.CloseSession(null); rtpCts?.Cancel(); } else if (sipRequest.Method == SIPMethodsEnum.SUBSCRIBE) { SIPResponse notAllowededResponse = SIPResponse.GetResponse(sipRequest, SIPResponseStatusCodesEnum.MethodNotAllowed, null); await sipTransport.SendResponseAsync(notAllowededResponse); } else if (sipRequest.Method == SIPMethodsEnum.OPTIONS || sipRequest.Method == SIPMethodsEnum.REGISTER) { SIPResponse optionsResponse = SIPResponse.GetResponse(sipRequest, SIPResponseStatusCodesEnum.Ok, null); await sipTransport.SendResponseAsync(optionsResponse); } } catch (Exception reqExcp) { SIPSorcery.Sys.Log.Logger.LogWarning($"Exception handling {sipRequest.Method}. {reqExcp.Message}"); } }; ManualResetEvent exitMre = new ManualResetEvent(false); Console.CancelKeyPress += delegate(object sender, ConsoleCancelEventArgs e) { e.Cancel = true; SIPSorcery.Sys.Log.Logger.LogInformation("Exiting..."); Hangup(uas).Wait(); rtpSession?.CloseSession(null); rtpCts?.Cancel(); if (sipTransport != null) { SIPSorcery.Sys.Log.Logger.LogInformation("Shutting down SIP transport..."); sipTransport.Shutdown(); } exitMre.Set(); }; // Task to handle user key presses. Task.Run(() => { try { while (!exitMre.WaitOne(0)) { var keyProps = Console.ReadKey(); if (keyProps.KeyChar == 'h' || keyProps.KeyChar == 'q') { Console.WriteLine(); Console.WriteLine("Hangup requested by user..."); Hangup(uas).Wait(); rtpSession?.CloseSession(null); rtpCts?.Cancel(); } if (keyProps.KeyChar == 'q') { SIPSorcery.Sys.Log.Logger.LogInformation("Quitting..."); if (sipTransport != null) { SIPSorcery.Sys.Log.Logger.LogInformation("Shutting down SIP transport..."); sipTransport.Shutdown(); } exitMre.Set(); } } } catch (Exception excp) { SIPSorcery.Sys.Log.Logger.LogError($"Exception Key Press listener. {excp.Message}."); } }); exitMre.WaitOne(); }
static void Main() { Console.WriteLine("SIPSorcery Call Hold and Blind Transfer example."); Console.WriteLine("Press 'c' to initiate a call to the default destination."); Console.WriteLine("Press 'h' to place an established call on and off hold."); Console.WriteLine("Press 'H' to hangup an established call."); Console.WriteLine("Press 't' to request a blind transfer on an established call."); Console.WriteLine("Press 'q' or ctrl-c to exit."); // Plumbing code to facilitate a graceful exit. CancellationTokenSource exitCts = new CancellationTokenSource(); // Cancellation token to stop the SIP transport and RTP stream. AddConsoleLogger(); // Set up a default SIP transport. var sipTransport = new SIPTransport(); sipTransport.AddSIPChannel(new SIPUDPChannel(new IPEndPoint(IPAddress.Any, SIP_LISTEN_PORT))); Console.WriteLine($"Listening for incoming calls on: {sipTransport.GetSIPChannels().First().ListeningEndPoint}."); EnableTraceLogs(sipTransport); _currentDir = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location); RtpAVSession rtpAVSession = null; // Create a client/server user agent to place a call to a remote SIP server along with event handlers for the different stages of the call. var userAgent = new SIPUserAgent(sipTransport, null); userAgent.RemotePutOnHold += () => Log.LogInformation("Remote call party has placed us on hold."); userAgent.RemoteTookOffHold += () => Log.LogInformation("Remote call party took us off hold."); sipTransport.SIPTransportRequestReceived += async(localEndPoint, remoteEndPoint, sipRequest) => { if (sipRequest.Header.From != null && sipRequest.Header.From.FromTag != null && sipRequest.Header.To != null && sipRequest.Header.To.ToTag != null) { // This is an in-dialog request that will be handled directly by a user agent instance. } else if (sipRequest.Method == SIPMethodsEnum.INVITE) { if (userAgent?.IsCallActive == true) { Log.LogWarning($"Busy response returned for incoming call request from {remoteEndPoint}: {sipRequest.StatusLine}."); // If we are already on a call return a busy response. UASInviteTransaction uasTransaction = new UASInviteTransaction(sipTransport, sipRequest, null); SIPResponse busyResponse = SIPResponse.GetResponse(sipRequest, SIPResponseStatusCodesEnum.BusyHere, null); uasTransaction.SendFinalResponse(busyResponse); } else { Log.LogInformation($"Incoming call request from {remoteEndPoint}: {sipRequest.StatusLine}."); var incomingCall = userAgent.AcceptCall(sipRequest); rtpAVSession = new RtpAVSession(new AudioOptions { AudioSource = AudioSourcesEnum.CaptureDevice }, null); await userAgent.Answer(incomingCall, rtpAVSession); Log.LogInformation($"Answered incoming call from {sipRequest.Header.From.FriendlyDescription()} at {remoteEndPoint}."); } } else { Log.LogDebug($"SIP {sipRequest.Method} request received but no processing has been set up for it, rejecting."); SIPResponse notAllowedResponse = SIPResponse.GetResponse(sipRequest, SIPResponseStatusCodesEnum.MethodNotAllowed, null); await sipTransport.SendResponseAsync(notAllowedResponse); } }; // At this point the call has been initiated and everything will be handled in an event handler. Task.Run(async() => { try { while (!exitCts.Token.WaitHandle.WaitOne(0)) { var keyProps = Console.ReadKey(); if (keyProps.KeyChar == 'c') { if (!userAgent.IsCallActive) { rtpAVSession = new RtpAVSession(new AudioOptions { AudioSource = AudioSourcesEnum.CaptureDevice }, null); bool callResult = await userAgent.Call(DEFAULT_DESTINATION_SIP_URI, SIP_USERNAME, SIP_PASSWORD, rtpAVSession); Log.LogInformation($"Call attempt {((callResult) ? "successfull" : "failed")}."); } else { Log.LogWarning("There is already an active call."); } } else if (keyProps.KeyChar == 'h') { // Place call on/off hold. if (userAgent.IsCallActive) { if (userAgent.IsOnLocalHold) { Log.LogInformation("Taking the remote call party off hold."); userAgent.TakeOffHold(); await(userAgent.MediaSession as RtpAVSession).SetSources(new AudioOptions { AudioSource = AudioSourcesEnum.CaptureDevice }, null); } else { Log.LogInformation("Placing the remote call party on hold."); userAgent.PutOnHold(); await(userAgent.MediaSession as RtpAVSession).SetSources(new AudioOptions { AudioSource = AudioSourcesEnum.Music, SourceFiles = new Dictionary <SDPMediaFormatsEnum, string> { { SDPMediaFormatsEnum.PCMU, _currentDir + "/" + AUDIO_FILE_PCMU } } }, null); } } else { Log.LogWarning("There is no active call to put on hold."); } } else if (keyProps.KeyChar == 'H') { if (userAgent.IsCallActive) { Log.LogInformation("Hanging up call."); userAgent.Hangup(); } } else if (keyProps.KeyChar == 't') { // Initiate a blind transfer to the remote call party. if (userAgent.IsCallActive) { var transferURI = SIPURI.ParseSIPURI(TRANSFER_DESTINATION_SIP_URI); bool result = await userAgent.BlindTransfer(transferURI, TimeSpan.FromSeconds(TRANSFER_TIMEOUT_SECONDS), exitCts.Token); if (result) { // If the transfer was accepted the original call will already have been hungup. // Wait a second for the transfer NOTIFY request to arrive. await Task.Delay(1000); exitCts.Cancel(); } else { Log.LogWarning($"Transfer to {TRANSFER_DESTINATION_SIP_URI} failed."); } } else { Log.LogWarning("There is no active call to transfer."); } } else if (keyProps.KeyChar == 'q') { // Quit application. exitCts.Cancel(); } } } catch (Exception excp) { SIPSorcery.Sys.Log.Logger.LogError($"Exception Key Press listener. {excp.Message}."); } }); // Ctrl-c will gracefully exit the call at any point. Console.CancelKeyPress += delegate(object sender, ConsoleCancelEventArgs e) { e.Cancel = true; exitCts.Cancel(); }; // Wait for a signal saying the call failed, was cancelled with ctrl-c or completed. exitCts.Token.WaitHandle.WaitOne(); #region Cleanup. Log.LogInformation("Exiting..."); rtpAVSession?.Close("app exit"); if (userAgent != null) { if (userAgent.IsCallActive) { Log.LogInformation($"Hanging up call to {userAgent?.CallDescriptor?.To}."); userAgent.Hangup(); } // Give the BYE or CANCEL request time to be transmitted. Log.LogInformation("Waiting 1s for call to clean up..."); Task.Delay(1000).Wait(); } SIPSorcery.Net.DNSManager.Stop(); if (sipTransport != null) { Log.LogInformation("Shutting down SIP transport..."); sipTransport.Shutdown(); } #endregion }
static async Task Main() { Console.WriteLine("SIPSorcery Send DTMF Tones example."); Console.WriteLine("Press ctrl-c to exit."); // Plumbing code to facilitate a graceful exit. CancellationTokenSource rtpCts = new CancellationTokenSource(); // Cancellation token to stop the RTP stream. AddConsoleLogger(); var sipTransport = new SIPTransport(); var userAgent = new SIPUserAgent(sipTransport, null); var rtpSession = new RtpAVSession(new AudioOptions { AudioSource = AudioSourcesEnum.CaptureDevice }, null); // Place the call and wait for the result. bool callResult = await userAgent.Call(DEFAULT_DESTINATION_SIP_URI, null, null, rtpSession); // Ctrl-c will gracefully exit the call at any point. Console.CancelKeyPress += delegate(object sender, ConsoleCancelEventArgs e) { e.Cancel = true; rtpCts.Cancel(); }; if (callResult) { Console.WriteLine("Call attempt successful."); // Give the call some time to answer. await Task.Delay(1000); // Send the DTMF tones. await userAgent.SendDtmf(0x05); await Task.Delay(2000); await userAgent.SendDtmf(0x09); await Task.Delay(2000); await userAgent.SendDtmf(0x02); await Task.Delay(2000); if (userAgent.IsCallActive) { Console.WriteLine("Hanging up."); rtpCts.Cancel(); userAgent.Hangup(); } } else { Console.WriteLine("Call attempt failed."); } Log.LogInformation("Exiting..."); // Clean up. sipTransport.Shutdown(); SIPSorcery.Net.DNSManager.Stop(); }
static async Task Main(string[] args) { Console.WriteLine("SIPSorcery client user agent example."); Console.WriteLine("Press ctrl-c to exit."); // Plumbing code to facilitate a graceful exit. ManualResetEvent exitMre = new ManualResetEvent(false); bool isCallHungup = false; bool hasCallFailed = false; AddConsoleLogger(); SIPURI callUri = SIPURI.ParseSIPURI(DEFAULT_DESTINATION_SIP_URI); if (args != null && args.Length > 0) { if (!SIPURI.TryParse(args[0], out callUri)) { Log.LogWarning($"Command line argument could not be parsed as a SIP URI {args[0]}"); } } Log.LogInformation($"Call destination {callUri}."); // Set up a default SIP transport. var sipTransport = new SIPTransport(); EnableTraceLogs(sipTransport); // Get the IP address the RTP will be sent from. While we can listen on IPAddress.Any | IPv6Any // we can't put 0.0.0.0 or [::0] in the SDP or the callee will treat our RTP stream as inactive. var lookupResult = SIPDNSManager.ResolveSIPService(callUri, false); Log.LogDebug($"DNS lookup result for {callUri}: {lookupResult?.GetSIPEndPoint()}."); var dstAddress = lookupResult.GetSIPEndPoint().Address; // Initialise an RTP session to receive the RTP packets from the remote SIP server. var rtpSession = new RtpAVSession(dstAddress.AddressFamily, new AudioOptions { AudioSource = AudioSourcesEnum.Microphone }, null); var offerSDP = await rtpSession.createOffer(new RTCOfferOptions { RemoteSignallingAddress = dstAddress }); // Create a client user agent to place a call to a remote SIP server along with event handlers for the different stages of the call. var uac = new SIPClientUserAgent(sipTransport); uac.CallTrying += (uac, resp) => Log.LogInformation($"{uac.CallDescriptor.To} Trying: {resp.StatusCode} {resp.ReasonPhrase}."); uac.CallRinging += (uac, resp) => Log.LogInformation($"{uac.CallDescriptor.To} Ringing: {resp.StatusCode} {resp.ReasonPhrase}."); uac.CallFailed += (uac, err) => { Log.LogWarning($"{uac.CallDescriptor.To} Failed: {err}"); hasCallFailed = true; }; uac.CallAnswered += (uac, resp) => { if (resp.Status == SIPResponseStatusCodesEnum.Ok) { Log.LogInformation($"{uac.CallDescriptor.To} Answered: {resp.StatusCode} {resp.ReasonPhrase}."); rtpSession.setRemoteDescription(new RTCSessionDescription { type = RTCSdpType.answer, sdp = SDP.ParseSDPDescription(resp.Body) }); rtpSession.Start(); } else { Log.LogWarning($"{uac.CallDescriptor.To} Answered: {resp.StatusCode} {resp.ReasonPhrase}."); } }; // The only incoming request that needs to be explicitly handled for this example is if the remote end hangs up the call. sipTransport.SIPTransportRequestReceived += async(SIPEndPoint localSIPEndPoint, SIPEndPoint remoteEndPoint, SIPRequest sipRequest) => { if (sipRequest.Method == SIPMethodsEnum.BYE) { SIPResponse okResponse = SIPResponse.GetResponse(sipRequest, SIPResponseStatusCodesEnum.Ok, null); await sipTransport.SendResponseAsync(okResponse); if (uac.IsUACAnswered) { Log.LogInformation("Call was hungup by remote server."); isCallHungup = true; exitMre.Set(); } } }; // Start the thread that places the call. SIPCallDescriptor callDescriptor = new SIPCallDescriptor( SIPConstants.SIP_DEFAULT_USERNAME, null, callUri.ToString(), SIPConstants.SIP_DEFAULT_FROMURI, callUri.CanonicalAddress, null, null, null, SIPCallDirection.Out, SDP.SDP_MIME_CONTENTTYPE, offerSDP.ToString(), null); uac.Call(callDescriptor); uac.ServerTransaction.TransactionTraceMessage += (tx, msg) => Log.LogInformation($"UAC tx trace message. {msg}"); // Ctrl-c will gracefully exit the call at any point. Console.CancelKeyPress += delegate(object sender, ConsoleCancelEventArgs e) { e.Cancel = true; exitMre.Set(); }; // Wait for a signal saying the call failed, was cancelled with ctrl-c or completed. exitMre.WaitOne(); Log.LogInformation("Exiting..."); rtpSession.CloseSession(null); if (!isCallHungup && uac != null) { if (uac.IsUACAnswered) { Log.LogInformation($"Hanging up call to {uac.CallDescriptor.To}."); uac.Hangup(); } else if (!hasCallFailed) { Log.LogInformation($"Cancelling call to {uac.CallDescriptor.To}."); uac.Cancel(); } // Give the BYE or CANCEL request time to be transmitted. Log.LogInformation("Waiting 1s for call to clean up..."); Task.Delay(1000).Wait(); } SIPSorcery.Net.DNSManager.Stop(); if (sipTransport != null) { Log.LogInformation("Shutting down SIP transport..."); sipTransport.Shutdown(); } }
static async Task Main() { Console.WriteLine("SIPSorcery Getting Started Demo"); AddConsoleLogger(); _waveFile = new WaveFileWriter("output.mp3", _waveFormat); var sipTransport = new SIPTransport(); var userAgent = new SIPUserAgent(sipTransport, null); userAgent.ClientCallFailed += (uac, err, resp) => { Console.WriteLine($"Call failed {err}"); _waveFile?.Close(); }; userAgent.OnCallHungup += (dialog) => _waveFile?.Close(); var rtpSession = new RtpAVSession( new AudioOptions { AudioSource = AudioSourcesEnum.CaptureDevice, AudioCodecs = new List <SDPMediaFormatsEnum> { SDPMediaFormatsEnum.PCMU, SDPMediaFormatsEnum.PCMA } }, null); rtpSession.OnRtpPacketReceived += OnRtpPacketReceived; // Ctrl-c will gracefully exit the call at any point. Console.CancelKeyPress += delegate(object sender, ConsoleCancelEventArgs e) { e.Cancel = true; if (userAgent.IsCallActive) { Console.WriteLine("Hanging up."); userAgent.Hangup(); } else { Console.WriteLine("Cancelling call"); userAgent.Cancel(); } }; // Place the call and wait for the result. bool callResult = await userAgent.Call(DESTINATION, null, null, rtpSession); Console.WriteLine($"Call result {((callResult) ? "success" : "failure")}."); Console.WriteLine("press any key to exit..."); Console.Read(); if (userAgent.IsCallActive) { Console.WriteLine("Hanging up."); userAgent.Hangup(); } // Clean up. sipTransport.Shutdown(); }
//private delegate void MediaSampleReadyDelegate(SDPMediaTypesEnum mediaType, uint duration, byte[] sample); //private static event MediaSampleReadyDelegate OnMediaFromSIPSampleReady; static void Main(string[] args) { Console.WriteLine("SIPSorcery SIP to WebRTC example."); Console.WriteLine("Press ctrl-c to exit."); // Plumbing code to facilitate a graceful exit. CancellationTokenSource exitCts = new CancellationTokenSource(); // Cancellation token to stop the SIP transport and RTP stream. AddConsoleLogger(); // Start web socket. Console.WriteLine("Starting web socket server..."); _webSocketServer = new WebSocketServer(IPAddress.Any, WEBSOCKET_PORT, true); _webSocketServer.SslConfiguration.ServerCertificate = new X509Certificate2(WEBSOCKET_CERTIFICATE_PATH); _webSocketServer.SslConfiguration.CheckCertificateRevocation = false; //_webSocketServer.Log.Level = WebSocketSharp.LogLevel.Debug; _webSocketServer.AddWebSocketService <SDPExchange>("/", (sdpExchanger) => { sdpExchanger.WebSocketOpened += SendSDPOffer; sdpExchanger.SDPAnswerReceived += SDPAnswerReceived; }); _webSocketServer.Start(); Console.WriteLine($"Waiting for browser web socket connection to {_webSocketServer.Address}:{_webSocketServer.Port}..."); // Set up a default SIP transport. var sipTransport = new SIPTransport(); sipTransport.AddSIPChannel(new SIPUDPChannel(new IPEndPoint(IPAddress.Any, SIP_LISTEN_PORT))); //EnableTraceLogs(sipTransport); RtpAVSession rtpAVSession = null; // Create a SIP user agent to receive a call from a remote SIP client. // Wire up event handlers for the different stages of the call. var userAgent = new SIPUserAgent(sipTransport, null); // We're only answering SIP calls, not placing them. userAgent.OnCallHungup += (dialog) => { Log.LogInformation($"Call hungup by remote party."); exitCts.Cancel(); }; userAgent.ServerCallCancelled += (uas) => Log.LogInformation("Incoming call cancelled by caller."); sipTransport.SIPTransportRequestReceived += async(localEndPoint, remoteEndPoint, sipRequest) => { if (sipRequest.Header.From != null && sipRequest.Header.From.FromTag != null && sipRequest.Header.To != null && sipRequest.Header.To.ToTag != null) { // This is an in-dialog request that will be handled directly by a user agent instance. } else if (sipRequest.Method == SIPMethodsEnum.INVITE) { if (userAgent?.IsCallActive == true) { Log.LogWarning($"Busy response returned for incoming call request from {remoteEndPoint}: {sipRequest.StatusLine}."); // If we are already on a call return a busy response. UASInviteTransaction uasTransaction = new UASInviteTransaction(sipTransport, sipRequest, null); SIPResponse busyResponse = SIPResponse.GetResponse(sipRequest, SIPResponseStatusCodesEnum.BusyHere, null); uasTransaction.SendFinalResponse(busyResponse); } else { Log.LogInformation($"Incoming call request from {remoteEndPoint}: {sipRequest.StatusLine}."); var incomingCall = userAgent.AcceptCall(sipRequest); rtpAVSession = new RtpAVSession(new AudioOptions { AudioSource = AudioSourcesEnum.CaptureDevice }, null); await userAgent.Answer(incomingCall, rtpAVSession); rtpAVSession.OnRtpPacketReceived += (mediaType, rtpPacket) => ForwardMedia(mediaType, rtpPacket); Log.LogInformation($"Answered incoming call from {sipRequest.Header.From.FriendlyDescription()} at {remoteEndPoint}."); } } else { Log.LogDebug($"SIP {sipRequest.Method} request received but no processing has been set up for it, rejecting."); SIPResponse notAllowedResponse = SIPResponse.GetResponse(sipRequest, SIPResponseStatusCodesEnum.MethodNotAllowed, null); await sipTransport.SendResponseAsync(notAllowedResponse); } }; // Ctrl-c will gracefully exit the call at any point. Console.CancelKeyPress += delegate(object sender, ConsoleCancelEventArgs e) { e.Cancel = true; exitCts.Cancel(); }; // Wait for a signal saying the call failed, was cancelled with ctrl-c or completed. exitCts.Token.WaitHandle.WaitOne(); #region Cleanup. Log.LogInformation("Exiting..."); rtpAVSession?.Close("app exit"); if (userAgent != null) { if (userAgent.IsCallActive) { Log.LogInformation($"Hanging up call to {userAgent?.CallDescriptor?.To}."); userAgent.Hangup(); } // Give the BYE or CANCEL request time to be transmitted. Log.LogInformation("Waiting 1s for call to clean up..."); Task.Delay(1000).Wait(); } SIPSorcery.Net.DNSManager.Stop(); if (sipTransport != null) { Log.LogInformation("Shutting down SIP transport..."); sipTransport.Shutdown(); } #endregion }
static void Main() { Console.WriteLine("SIPSorcery Attended Transfer example."); Console.WriteLine("Press 'c' to place a call to the default destination."); Console.WriteLine("Place two simultaneous SIP calls to this program and then press 't'."); Console.WriteLine("Press 'q' or ctrl-c to exit."); // Plumbing code to facilitate a graceful exit. CancellationTokenSource exitCts = new CancellationTokenSource(); // Cancellation token to stop the SIP transport and RTP stream. AddConsoleLogger(); // Set up a default SIP transport. var sipTransport = new SIPTransport(); sipTransport.AddSIPChannel(new SIPUDPChannel(new IPEndPoint(IPAddress.Any, SIP_LISTEN_PORT))); //EnableTraceLogs(sipTransport); // Create two user agents. Each gets configured to answer an incoming call. var userAgent1 = new SIPUserAgent(sipTransport, null); var userAgent2 = new SIPUserAgent(sipTransport, null); userAgent1.OnCallHungup += (dialog) => Log.LogInformation($"UA1: Call hungup by remote party."); userAgent1.ServerCallCancelled += (uas) => Log.LogInformation("UA1: Incoming call cancelled by caller."); userAgent2.OnCallHungup += (dialog) => Log.LogInformation($"UA2: Call hungup by remote party."); userAgent2.ServerCallCancelled += (uas) => Log.LogInformation("UA2: Incoming call cancelled by caller."); userAgent2.OnTransferNotify += (sipFrag) => { if (!string.IsNullOrEmpty(sipFrag)) { Log.LogInformation($"UA2: Transfer status update: {sipFrag.Trim()}."); if (sipFrag?.Contains("SIP/2.0 200") == true) { // The transfer attempt got a successful answer. Can hangup the call. userAgent2.Hangup(); exitCts.Cancel(); } } }; sipTransport.SIPTransportRequestReceived += async(localEndPoint, remoteEndPoint, sipRequest) => { if (sipRequest.Header.From != null && sipRequest.Header.From.FromTag != null && sipRequest.Header.To != null && sipRequest.Header.To.ToTag != null) { // This is an in-dialog request that will be handled directly by a user agent instance. } else if (sipRequest.Method == SIPMethodsEnum.INVITE) { if (!userAgent1.IsCallActive || !userAgent2.IsCallActive) { SIPUserAgent activeAgent = (!userAgent1.IsCallActive) ? userAgent1 : userAgent2; string agentDesc = (!userAgent1.IsCallActive) ? "UA1" : "UA2"; Log.LogInformation($"{agentDesc}: Incoming call request from {remoteEndPoint}: {sipRequest.StatusLine}."); var incomingCall = activeAgent.AcceptCall(sipRequest); var rtpAVSession = new RtpAVSession(new AudioOptions { AudioSource = AudioSourcesEnum.CaptureDevice }, null); await activeAgent.Answer(incomingCall, rtpAVSession); Log.LogInformation($"{agentDesc}: Answered incoming call from {sipRequest.Header.From.FriendlyDescription()} at {remoteEndPoint}."); } else { // If both user agents are already on a call return a busy response. Log.LogWarning($"Busy response returned for incoming call request from {remoteEndPoint}: {sipRequest.StatusLine}."); UASInviteTransaction uasTransaction = new UASInviteTransaction(sipTransport, sipRequest, null); SIPResponse busyResponse = SIPResponse.GetResponse(sipRequest, SIPResponseStatusCodesEnum.BusyHere, null); uasTransaction.SendFinalResponse(busyResponse); } } else { Log.LogDebug($"SIP {sipRequest.Method} request received but no processing has been set up for it, rejecting."); SIPResponse notAllowedResponse = SIPResponse.GetResponse(sipRequest, SIPResponseStatusCodesEnum.MethodNotAllowed, null); await sipTransport.SendResponseAsync(notAllowedResponse); } }; // At this point the call has been initiated and everything will be handled in an event handler. Task.Run(async() => { try { while (!exitCts.Token.WaitHandle.WaitOne(0)) { var keyProps = Console.ReadKey(); if (keyProps.KeyChar == 'c') { // Place an outgoing call using the first free user agent. SIPUserAgent freeAgent = (!userAgent1.IsCallActive) ? userAgent1 : (!userAgent2.IsCallActive) ? userAgent2 : null; if (freeAgent != null) { var rtpAVSession = new RtpAVSession(new AudioOptions { AudioSource = AudioSourcesEnum.CaptureDevice }, null); bool callResult = await freeAgent.Call(DEFAULT_DESTINATION_SIP_URI, null, null, rtpAVSession); Log.LogInformation($"Call attempt {((callResult) ? "successfull" : "failed")}."); } else { Log.LogWarning("Both user agents are already on calls."); } } if (keyProps.KeyChar == 't') { // Initiate the attended transfer. if (userAgent1.IsCallActive && userAgent2.IsCallActive) { bool result = await userAgent2.AttendedTransfer(userAgent1.Dialogue, TimeSpan.FromSeconds(TRANSFER_TIMEOUT_SECONDS), exitCts.Token); if (!result) { Log.LogWarning($"Attended transfer failed."); } } else { Log.LogWarning("There need to be two active calls before the attended transfer can occur."); } } else if (keyProps.KeyChar == 'q') { // Quit application. exitCts.Cancel(); } } } catch (Exception excp) { SIPSorcery.Sys.Log.Logger.LogError($"Exception Key Press listener. {excp.Message}."); } }); // Ctrl-c will gracefully exit the call at any point. Console.CancelKeyPress += delegate(object sender, ConsoleCancelEventArgs e) { e.Cancel = true; exitCts.Cancel(); }; // Wait for a signal saying the call failed, was cancelled with ctrl-c or completed. exitCts.Token.WaitHandle.WaitOne(); #region Cleanup. Log.LogInformation("Exiting..."); userAgent1?.Hangup(); userAgent2?.Hangup(); // Give any BYE or CANCEL requests time to be transmitted. Log.LogInformation("Waiting 1s for calls to be cleaned up..."); Task.Delay(1000).Wait(); if (sipTransport != null) { Log.LogInformation("Shutting down SIP transport..."); sipTransport.Shutdown(); } #endregion }
static void Main() { Console.WriteLine("SIPSorcery Getting Started Video Call Demo"); Console.WriteLine("Press ctrl-c to exit."); AddConsoleLogger(); _sipTransport = new SIPTransport(); EnableTraceLogs(_sipTransport); // Open a Window to display the video feed from the WebRTC peer. _form = new Form(); _form.AutoSize = true; _form.BackgroundImageLayout = ImageLayout.Center; _picBox = new PictureBox { Size = new Size(640, 480), Location = new Point(0, 0), Visible = true }; _form.Controls.Add(_picBox); Application.EnableVisualStyles(); ThreadPool.QueueUserWorkItem(delegate { Application.Run(_form); }); ManualResetEvent formMre = new ManualResetEvent(false); _form.Activated += (object sender, EventArgs e) => formMre.Set(); Console.WriteLine("Waiting for form activation."); formMre.WaitOne(); _sipTransport.SIPTransportRequestReceived += OnSIPTransportRequestReceived; string executableDir = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location); var userAgent = new SIPUserAgent(_sipTransport, null); var audioSrcOpts = new AudioOptions { AudioSource = AudioSourcesEnum.Music, SourceFiles = new Dictionary <SDPMediaFormatsEnum, string> { { SDPMediaFormatsEnum.PCMU, executableDir + "/" + AUDIO_FILE_PCMU } } }; var videoSrcOpts = new VideoOptions { VideoSource = VideoSourcesEnum.TestPattern, SourceFile = executableDir + "/" + VIDEO_TEST_PATTERN_FILE }; var rtpSession = new RtpAVSession(audioSrcOpts, videoSrcOpts); // Place the call and wait for the result. Task <bool> callTask = userAgent.Call(DESTINATION, null, null, rtpSession); callTask.Wait(CALL_IMTEOUT_SECONDS * 1000); ManualResetEvent exitMRE = new ManualResetEvent(false); if (callTask.Result) { Log.LogInformation("Call attempt successful."); rtpSession.OnVideoSampleReady += (byte[] sample, uint width, uint height, int stride) => { _picBox.BeginInvoke(new Action(() => { unsafe { fixed(byte *s = sample) { System.Drawing.Bitmap bmpImage = new System.Drawing.Bitmap((int)width, (int)height, stride, System.Drawing.Imaging.PixelFormat.Format24bppRgb, (IntPtr)s); _picBox.Image = bmpImage; } } })); }; } else { Log.LogWarning("Call attempt failed."); Console.WriteLine("Press ctrl-c to exit."); } Console.CancelKeyPress += delegate(object sender, ConsoleCancelEventArgs e) { e.Cancel = true; Log.LogInformation("Exiting..."); exitMRE.Set(); }; exitMRE.WaitOne(); if (userAgent.IsCallActive) { Log.LogInformation("Hanging up."); userAgent.Hangup(); Task.Delay(1000).Wait(); } // Clean up. _form.BeginInvoke(new Action(() => _form.Close())); _sipTransport.Shutdown(); }