/// <summary> /// Sends the sounds of silence. If the destination is on the other side of a NAT this is useful to open /// a pinhole and hopefully get the remote RTP stream through. /// </summary> /// <param name="rtpMediaSession">Our RTP sending session.</param> /// <param name="cts">Cancellation token to stop the call.</param> private static async void SendSilence(RTPMediaSession rtpMediaSession, CancellationTokenSource cts) { int samplingFrequency = rtpMediaSession.MediaAnnouncements.First().MediaFormats.First().GetClockRate(); uint rtpTimestampStep = (uint)(samplingFrequency * SILENCE_SAMPLE_PERIOD / 1000); uint bufferSize = (uint)SILENCE_SAMPLE_PERIOD; uint rtpSampleTimestamp = 0; while (cts.IsCancellationRequested == false) { if (rtpMediaSession.DestinationEndPoint != null) { byte[] sample = new byte[bufferSize / 2]; int sampleIndex = 0; for (int index = 0; index < bufferSize; index += 2) { sample[sampleIndex] = PCMU_SILENCE_BYTE_ZERO; sample[sampleIndex + 1] = PCMU_SILENCE_BYTE_ONE; } rtpMediaSession.SendAudioFrame(rtpSampleTimestamp, (int)SDPMediaFormatsEnum.PCMU, sample); rtpSampleTimestamp += rtpTimestampStep; } await Task.Delay(SILENCE_SAMPLE_PERIOD); } }
/// <summary> /// Connects the RTP packets we receive to the speaker and sends RTP packets for microphone samples. /// </summary> /// <param name="rtpSession">The RTP session to use for sending and receiving.</param> /// <param name="microphone">The default system audio input device found.</param> /// <param name="speaker">The default system audio output device.</param> private static void ConnectAudioDevicesToRtp(RTPMediaSession rtpSession, WaveInEvent microphone, BufferedWaveProvider speaker) { // Wire up the RTP send session to the audio input device. uint rtpSendTimestamp = 0; microphone.DataAvailable += (object sender, WaveInEventArgs args) => { byte[] sample = new byte[args.Buffer.Length / 2]; int sampleIndex = 0; for (int index = 0; index < args.BytesRecorded; index += 2) { var ulawByte = NAudio.Codecs.MuLawEncoder.LinearToMuLawSample(BitConverter.ToInt16(args.Buffer, index)); sample[sampleIndex++] = ulawByte; } if (rtpSession.DestinationEndPoint != null) { rtpSession.SendAudioFrame(rtpSendTimestamp, sample); rtpSendTimestamp += (uint)(8000 / microphone.BufferMilliseconds); } }; // Wire up the RTP receive session to the audio output device. rtpSession.OnReceivedSampleReady += (sample) => { for (int index = 0; index < sample.Length; index++) { short pcm = NAudio.Codecs.MuLawDecoder.MuLawToLinearSample(sample[index]); byte[] pcmSample = new byte[] { (byte)(pcm & 0xFF), (byte)(pcm >> 8) }; speaker.AddSamples(pcmSample, 0, 2); } }; }
/// <summary> /// Event handler for a default audio sample being ready from a local media source. /// The RTP media session Forwards samples from the local audio input device to RTP session. /// We leave it up to the RTP session to decide if it wants to transmit the sample or not. /// For example an RTP session will know whether it's on hold and whether it needs to send /// audio to the remote call party or not. /// </summary> /// <param name="sample">The audio sample</param> private void LocalAudioSampleReadyForSession(byte[] sample) { int payloadID = 0; // Convert.ToInt32(RTPMediaSession.MediaAnnouncements.First(x => x.Media == SDPMediaTypesEnum.audio).MediaFormats.First().FormatID); RTPMediaSession.SendAudioFrame(_audioTimestamp, payloadID, sample); _audioTimestamp += (uint)sample.Length; // This only works for cases where 1 sample is 1 byte. }
/// <summary> /// Creates a new RTP media session object. /// </summary> /// <param name="addressFamily">The type of socket the RTP session should use, IPv4 or IPv6.</param> /// <returns>A new RTP media session object.</returns> public virtual RTPMediaSession Create(AddressFamily addressFamily) { RTPMediaSession = new RTPMediaSession(SDPMediaTypesEnum.audio, new SDPMediaFormat(DefaultAudioFormat), addressFamily); RTPMediaSession.OnRtpClosed += (reason) => { _mediaManager.OnLocalAudioSampleReady -= LocalAudioSampleReadyForSession; _musicOnHold.OnAudioSampleReady -= LocalAudioSampleReadyForSession; }; _mediaManager.OnLocalAudioSampleReady += LocalAudioSampleReadyForSession; RTPMediaSession.OnRtpPacketReceived += RemoteRtpPacketReceived; return(RTPMediaSession); }
/// <summary> /// Creates a new RTP media session object. /// </summary> /// <param name="addressFamily">The type of socket the RTP session should use, IPv4 or IPv6.</param> /// <returns>A new RTP media session object.</returns> public virtual RTPMediaSession Create(AddressFamily addressFamily) { RTPMediaSession = new RTPMediaSession((int)DefaultAudioFormat, addressFamily); RTPMediaSession.OnRtpClosed += (reason) => { _mediaManager.OnLocalAudioSampleReady -= LocalAudioSampleReadyForSession; _musicOnHold.OnAudioSampleReady -= LocalAudioSampleReadyForSession; }; _mediaManager.OnLocalAudioSampleReady += LocalAudioSampleReadyForSession; RTPMediaSession.OnReceivedSampleReady += RemoteAudioSampleReceived; return(RTPMediaSession); }
/// <summary> /// Wires up the active RTP session to the speaker. /// </summary> /// <param name="rtpSession">The active RTP session receiving the remote party's RTP packets.</param> /// <param name="audioOutProvider">The audio buffer for the default system audio output device.</param> private static void PlayRemoteMedia(RTPMediaSession rtpSession, BufferedWaveProvider audioOutProvider) { if (rtpSession == null) { return; } rtpSession.OnReceivedSampleReady += (sample) => { for (int index = 0; index < sample.Length; index++) { short pcm = NAudio.Codecs.MuLawDecoder.MuLawToLinearSample(sample[index]); byte[] pcmSample = new byte[] { (byte)(pcm & 0xFF), (byte)(pcm >> 8) }; audioOutProvider.AddSamples(pcmSample, 0, 2); } }; }
static async Task Main() { Console.WriteLine("SIPSorcery Getting Started Demo"); AddConsoleLogger(); var sipTransport = new SIPTransport(); var userAgent = new SIPUserAgent(sipTransport, null); var rtpSession = new RTPMediaSession((int)SDPMediaFormatsEnum.PCMU, AddressFamily.InterNetwork); // Connect audio devices to RTP session. WaveInEvent microphone = GetAudioInputDevice(); var speaker = GetAudioOutputDevice(); ConnectAudioDevicesToRtp(rtpSession, microphone, speaker); // Place the call and wait for the result. bool callResult = await userAgent.Call(DESTINATION, null, null, rtpSession); if (callResult) { Console.WriteLine("Call attempt successful."); microphone.StartRecording(); } else { Console.WriteLine("Call attempt failed."); } Console.WriteLine("press any key to exit..."); Console.Read(); if (userAgent.IsCallActive) { Console.WriteLine("Hanging up."); userAgent.Hangup(); } // Clean up. microphone.StopRecording(); sipTransport.Shutdown(); SIPSorcery.Net.DNSManager.Stop(); }
static void Main() { Console.WriteLine("SIPSorcery call hold 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(); // 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); // Only one of the user agents can use the microphone and speaker. The one designated // as the active agent gets the devices. SIPUserAgent activeUserAgent = null; RTPMediaSession activeRtpSession = null; // Get the default speaker. var(audioOutEvent, audioOutProvider) = GetAudioOutputDevice(); m_audioOutProvider = audioOutProvider; WaveInEvent waveInEvent = GetAudioInputDevice(); userAgent1.OnCallHungup += () => Log.LogInformation($"UA1: Call hungup by remote party."); userAgent1.ServerCallCancelled += (uas) => Log.LogInformation("UA1: Incoming call cancelled by caller."); userAgent2.OnCallHungup += () => 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 succesful answer. Can hangup the call. userAgent2.Hangup(); exitCts.Cancel(); } } }; sipTransport.SIPTransportRequestReceived += (locelEndPoint, 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) { Log.LogInformation($"UA1: Incoming call request from {remoteEndPoint}: {sipRequest.StatusLine}."); var incomingCall = userAgent1.AcceptCall(sipRequest); var rtpMediaSession = new RTPMediaSession(SDPMediaTypesEnum.audio, new SDPMediaFormat(SDPMediaFormatsEnum.PCMU), AddressFamily.InterNetwork); rtpMediaSession.RemotePutOnHold += () => Log.LogInformation("UA1: Remote call party has placed us on hold."); rtpMediaSession.RemoteTookOffHold += () => Log.LogInformation("UA1: Remote call party took us off hold."); userAgent1.Answer(incomingCall, rtpMediaSession) .ContinueWith(task => { activeUserAgent = userAgent1; activeRtpSession = rtpMediaSession; activeRtpSession.OnRtpPacketReceived += PlaySample; waveInEvent.StartRecording(); Log.LogInformation($"UA1: Answered incoming call from {sipRequest.Header.From.FriendlyDescription()} at {remoteEndPoint}."); }, exitCts.Token); } else if (!userAgent2.IsCallActive) { Log.LogInformation($"UA2: Incoming call request from {remoteEndPoint}: {sipRequest.StatusLine}."); var incomingCall = userAgent2.AcceptCall(sipRequest); var rtpMediaSession = new RTPMediaSession(SDPMediaTypesEnum.audio, new SDPMediaFormat(SDPMediaFormatsEnum.PCMU), AddressFamily.InterNetwork); rtpMediaSession.RemotePutOnHold += () => Log.LogInformation("UA2: Remote call party has placed us on hold."); rtpMediaSession.RemoteTookOffHold += () => Log.LogInformation("UA2: Remote call party took us off hold."); userAgent2.Answer(incomingCall, rtpMediaSession) .ContinueWith(task => { activeRtpSession.OnRtpPacketReceived -= PlaySample; activeUserAgent = userAgent2; activeRtpSession = rtpMediaSession; activeRtpSession.PutOnHold(); activeRtpSession.OnRtpPacketReceived += PlaySample; Log.LogInformation($"UA2: Answered incoming call from {sipRequest.Header.From.FriendlyDescription()} at {remoteEndPoint}."); }, exitCts.Token); } 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); return(sipTransport.SendResponseAsync(notAllowedResponse)); } return(Task.FromResult(0)); }; // Wire up the RTP send session to the audio input device. uint rtpSendTimestamp = 0; waveInEvent.DataAvailable += (object sender, WaveInEventArgs args) => { byte[] sample = new byte[args.Buffer.Length / 2]; int sampleIndex = 0; for (int index = 0; index < args.BytesRecorded; index += 2) { var ulawByte = NAudio.Codecs.MuLawEncoder.LinearToMuLawSample(BitConverter.ToInt16(args.Buffer, index)); sample[sampleIndex++] = ulawByte; } if (activeRtpSession != null) { activeRtpSession.SendAudioFrame(rtpSendTimestamp, (int)SDPMediaFormatsEnum.PCMU, sample); rtpSendTimestamp += (uint)sample.Length; } }; // 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 == 't') { 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(); waveInEvent?.StopRecording(); audioOutEvent?.Stop(); // Give any BYE or CANCEL requests time to be transmitted. Log.LogInformation("Waiting 1s for calls to be cleaned 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 client user agent 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. bool isCallHungup = false; bool hasCallFailed = false; AddConsoleLogger(); SIPURI callUri = SIPURI.ParseSIPURI(DEFAULT_DESTINATION_SIP_URI); Log.LogInformation($"Call destination {callUri}."); // Set up a default SIP transport. var sipTransport = new SIPTransport(); sipTransport.AddSIPChannel(new SIPUDPChannel(new IPEndPoint(IPAddress.Any, 0))); // Un/comment this line to see/hide each SIP message sent and received. EnableTraceLogs(sipTransport); // Note this relies on the callURI host being an IP address. If it's a hostname a DNS lookup is required. IPAddress dstAddress = callUri.ToSIPEndPoint().Address; // Initialise an RTP session to receive the RTP packets from the remote SIP server. var rtpSession = new RTPMediaSession(SDPMediaTypesEnum.audio, new SDPMediaFormat(SDPMediaFormatsEnum.PCMU), dstAddress.AddressFamily); var offerSDP = await rtpSession.CreateOffer(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.SetRemoteSDP(SDP.ParseSDPDescription(resp.Body)); Log.LogDebug($"Remote RTP socket {rtpSession.DestinationEndPoint}."); } 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; rtpCts.Cancel(); } } }; // Wire up the RTP receive session to the default speaker. var(audioOutEvent, audioOutProvider) = GetAudioOutputDevice(); rtpSession.OnRtpPacketReceived += (mediaType, rtpPacket) => { var sample = rtpPacket.Payload; for (int index = 0; index < sample.Length; index++) { short pcm = NAudio.Codecs.MuLawDecoder.MuLawToLinearSample(sample[index]); byte[] pcmSample = new byte[] { (byte)(pcm & 0xFF), (byte)(pcm >> 8) }; audioOutProvider.AddSamples(pcmSample, 0, 2); } }; // Send audio packets (in this case silence) to the callee. Task.Run(() => SendSilence(rtpSession, rtpCts)); // Start the thread that places the call. SIPCallDescriptor callDescriptor = new SIPCallDescriptor( SIPConstants.SIP_DEFAULT_USERNAME, null, callUri.ToString(), SIPConstants.SIP_DEFAULT_FROMURI, null, null, null, null, SIPCallDirection.Out, SDP.SDP_MIME_CONTENTTYPE, offerSDP.ToString(), null); uac.Call(callDescriptor); // Ctrl-c will gracefully exit the call at any point. Console.CancelKeyPress += delegate(object sender, ConsoleCancelEventArgs e) { e.Cancel = true; rtpCts.Cancel(); }; // Give the call some time to answer. Task.Delay(3000).Wait(); // Send some DTMF key presses via RTP events. var dtmf5 = new RTPEvent(0x05, false, RTPEvent.DEFAULT_VOLUME, 1200, RTPMediaSession.DTMF_EVENT_PAYLOAD_ID); rtpSession.SendDtmfEvent(dtmf5, rtpCts.Token).Wait(); Task.Delay(2000, rtpCts.Token).Wait(); var dtmf9 = new RTPEvent(0x09, false, RTPEvent.DEFAULT_VOLUME, 1200, RTPMediaSession.DTMF_EVENT_PAYLOAD_ID); rtpSession.SendDtmfEvent(dtmf9, rtpCts.Token).Wait(); Task.Delay(2000, rtpCts.Token).Wait(); var dtmf2 = new RTPEvent(0x02, false, RTPEvent.DEFAULT_VOLUME, 1200, RTPMediaSession.DTMF_EVENT_PAYLOAD_ID); rtpSession.SendDtmfEvent(dtmf2, rtpCts.Token).Wait(); Task.Delay(2000, rtpCts.Token).ContinueWith((task) => { }).Wait(); // Don't care about the exception if the cancellation token is set. Log.LogInformation("Exiting..."); rtpCts.Cancel(); audioOutEvent?.Stop(); 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 void Main() { Console.WriteLine("SIPSorcery call hold 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. bool isCallHungup = false; bool hasCallFailed = false; AddConsoleLogger(); // Set up a default SIP transport. var sipTransport = new SIPTransport(); sipTransport.AddSIPChannel(new SIPUDPChannel(new IPEndPoint(IPAddress.Any, SIP_LISTEN_PORT))); //EnableTraceLogs(sipTransport); // Get the default speaker. var(audioOutEvent, audioOutProvider) = GetAudioOutputDevice(); WaveInEvent waveInEvent = GetAudioInputDevice(); RTPMediaSession RtpMediaSession = 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.ClientCallTrying += (uac, resp) => Log.LogInformation($"{uac.CallDescriptor.To} Trying: {resp.StatusCode} {resp.ReasonPhrase}."); userAgent.ClientCallRinging += (uac, resp) => Log.LogInformation($"{uac.CallDescriptor.To} Ringing: {resp.StatusCode} {resp.ReasonPhrase}."); userAgent.ClientCallFailed += (uac, err) => { Log.LogWarning($"{uac.CallDescriptor.To} Failed: {err}"); hasCallFailed = true; exitCts.Cancel(); }; userAgent.ClientCallAnswered += (uac, resp) => { if (resp.Status == SIPResponseStatusCodesEnum.Ok) { Log.LogInformation($"{uac.CallDescriptor.To} Answered: {resp.StatusCode} {resp.ReasonPhrase}."); PlayRemoteMedia(RtpMediaSession, audioOutProvider); } else { Log.LogWarning($"{uac.CallDescriptor.To} Answered: {resp.StatusCode} {resp.ReasonPhrase}."); hasCallFailed = true; exitCts.Cancel(); } }; userAgent.OnCallHungup += () => { 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); RtpMediaSession = new RTPMediaSession(SDPMediaTypesEnum.audio, (int)SDPMediaFormatsEnum.PCMU, AddressFamily.InterNetwork); RtpMediaSession.RemotePutOnHold += () => Log.LogInformation("Remote call party has placed us on hold."); RtpMediaSession.RemoteTookOffHold += () => Log.LogInformation("Remote call party took us off hold."); await userAgent.Answer(incomingCall, RtpMediaSession); PlayRemoteMedia(RtpMediaSession, audioOutProvider); waveInEvent.StartRecording(); 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); } }; // Wire up the RTP send session to the audio output device. uint rtpSendTimestamp = 0; waveInEvent.DataAvailable += (object sender, WaveInEventArgs args) => { byte[] sample = new byte[args.Buffer.Length / 2]; int sampleIndex = 0; for (int index = 0; index < args.BytesRecorded; index += 2) { var ulawByte = NAudio.Codecs.MuLawEncoder.LinearToMuLawSample(BitConverter.ToInt16(args.Buffer, index)); sample[sampleIndex++] = ulawByte; } if (RtpMediaSession != null) { RtpMediaSession.SendAudioFrame(rtpSendTimestamp, sample); rtpSendTimestamp += (uint)(8000 / waveInEvent.BufferMilliseconds); } }; // 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) { RtpMediaSession = new RTPMediaSession(SDPMediaTypesEnum.audio, (int)SDPMediaFormatsEnum.PCMU, AddressFamily.InterNetwork); RtpMediaSession.RemotePutOnHold += () => Log.LogInformation("Remote call party has placed us on hold."); RtpMediaSession.RemoteTookOffHold += () => Log.LogInformation("Remote call party took us off hold."); var callDescriptor = GetCallDescriptor(DEFAULT_DESTINATION_SIP_URI); await userAgent.InitiateCall(callDescriptor, RtpMediaSession); } else { Log.LogWarning("There is already an active call."); } } else if (keyProps.KeyChar == 'h') { // Place call on/off hold. if (userAgent.IsCallActive) { if (RtpMediaSession.LocalOnHold) { Log.LogInformation("Taking the remote call party off hold."); RtpMediaSession.TakeOffHold(); } else { Log.LogInformation("Placing the remote call party on hold."); RtpMediaSession.PutOnHold(); } } else { Log.LogWarning("There is no active call to put on hold."); } } else if (keyProps.KeyChar == 't') { 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..."); RtpMediaSession?.Close(); waveInEvent?.StopRecording(); audioOutEvent?.Stop(); if (!isCallHungup && userAgent != null) { if (userAgent.IsCallActive) { Log.LogInformation($"Hanging up call to {userAgent?.CallDescriptor?.To}."); userAgent.Hangup(); } else if (!hasCallFailed) { Log.LogInformation($"Cancelling call to {userAgent?.CallDescriptor?.To}."); userAgent.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(); } #endregion }
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); // 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. RTPMediaSession 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); string audioFile = null; if (offerSdp.Media.Any(x => x.Media == SDPMediaTypesEnum.audio && x.HasMediaFormat((int)SDPMediaFormatsEnum.G722))) { Log.LogDebug($"Using G722 RTP media type and audio file {AUDIO_FILE_G722}."); rtpSession = new RTPMediaSession(SDPMediaTypesEnum.audio, (int)SDPMediaFormatsEnum.G722, dstRtpEndPoint.AddressFamily); audioFile = AUDIO_FILE_G722; } else if (offerSdp.Media.Any(x => x.Media == SDPMediaTypesEnum.audio && x.HasMediaFormat((int)SDPMediaFormatsEnum.PCMU))) { Log.LogDebug($"Using PCMU RTP media type and audio file {AUDIO_FILE_PCMU}."); rtpSession = new RTPMediaSession(SDPMediaTypesEnum.audio, (int)SDPMediaFormatsEnum.PCMU, dstRtpEndPoint.AddressFamily); audioFile = AUDIO_FILE_PCMU; } 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); // The RTP socket is listening on IPAddress.Any but the IP address placed into the SDP needs to be one the caller can reach. IPAddress rtpAddress = NetServices.GetLocalAddressForRemote(dstRtpEndPoint.Address); // Only set the remote RTP end point if there hasn't already been a packet received on it. if (rtpSession.DestinationEndPoint == null) { rtpSession.SetRemoteSDP(SDP.ParseSDPDescription(sipRequest.Body)); Log.LogDebug($"Remote RTP socket {rtpSession.DestinationEndPoint}."); } rtpSession.SetRemoteSDP(SDP.ParseSDPDescription(sipRequest.Body)); _ = Task.Run(() => SendRtp(rtpSession, dstRtpEndPoint, audioFile, rtpCts)) .ContinueWith(_ => { if (uas?.IsHungup == false) { uas?.Hangup(false); rtpSession?.CloseSession(null); } }); uas.Answer(SDP.SDP_MIME_CONTENTTYPE, rtpSession.GetSDP(rtpAddress).ToString(), null, SIPDialogueTransferModesEnum.NotAllowed); } } 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(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); RTPMediaSession RtpMediaSession = 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 += () => { 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); RtpMediaSession = new RTPMediaSession(SDPMediaTypesEnum.audio, new SDPMediaFormat(SDPMediaFormatsEnum.PCMU), AddressFamily.InterNetwork); await userAgent.Answer(incomingCall, RtpMediaSession); RtpMediaSession.OnRtpPacketReceived += (mediaType, rtpPacket) => OnMediaSampleReady?.Invoke(mediaType, rtpPacket.Header.Timestamp, rtpPacket.Payload); 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..."); RtpMediaSession?.Close(); 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 }
/// <summary> /// Event handler for a default audio sample being ready from a local media source. /// The RTP media session Forwards samples from the local audio input device to RTP session. /// We leave it up to the RTP session to decide if it wants to transmit the sample or not. /// For example an RTP session will know whether it's on hold and whether it needs to send /// audio to the remote call party or not. /// </summary> /// <param name="sample">The audio sample</param> private void LocalAudioSampleReadyForSession(byte[] sample) { RTPMediaSession.SendAudioFrame(_audioTimestamp, sample); _audioTimestamp += (uint)sample.Length; // This only works for cases where 1 sample is 1 byte. }
private static int INPUT_SAMPLE_PERIOD_MILLISECONDS = 20; // This sets the frequency of the RTP packets. static void 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 ignore us. var lookupResult = SIPDNSManager.ResolveSIPService(callUri, false); Log.LogDebug($"DNS lookup result for {callUri}: {lookupResult?.GetSIPEndPoint()}."); var dstAddress = lookupResult.GetSIPEndPoint().Address; IPAddress localIPAddress = NetServices.GetLocalAddressForRemote(dstAddress); // Initialise an RTP session to receive the RTP packets from the remote SIP server. var rtpSession = new RTPMediaSession((int)SDPMediaFormatsEnum.PCMU, localIPAddress.AddressFamily); var offerSDP = rtpSession.GetSDP(localIPAddress); // Get the audio input device. WaveInEvent waveInEvent = GetAudioInputDevice(); // 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}."); // Only set the remote RTP end point if there hasn't already been a packet received on it. if (rtpSession.DestinationEndPoint == null) { rtpSession.DestinationEndPoint = SDP.GetSDPRTPEndPoint(resp.Body); Log.LogDebug($"Remote RTP socket {rtpSession.DestinationEndPoint}."); } rtpSession.SetRemoteSDP(SDP.ParseSDPDescription(resp.Body)); waveInEvent.StartRecording(); } 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(); } } }; // Wire up the RTP receive session to the audio output device. var(audioOutEvent, audioOutProvider) = GetAudioOutputDevice(); rtpSession.OnReceivedSampleReady += (sample) => { for (int index = 0; index < sample.Length; index++) { short pcm = NAudio.Codecs.MuLawDecoder.MuLawToLinearSample(sample[index]); byte[] pcmSample = new byte[] { (byte)(pcm & 0xFF), (byte)(pcm >> 8) }; audioOutProvider.AddSamples(pcmSample, 0, 2); } }; // Wire up the RTP send session to the audio input device. uint rtpSendTimestamp = 0; waveInEvent.DataAvailable += (object sender, WaveInEventArgs args) => { byte[] sample = new byte[args.Buffer.Length / 2]; int sampleIndex = 0; for (int index = 0; index < args.BytesRecorded; index += 2) { var ulawByte = NAudio.Codecs.MuLawEncoder.LinearToMuLawSample(BitConverter.ToInt16(args.Buffer, index)); sample[sampleIndex++] = ulawByte; } if (rtpSession.DestinationEndPoint != null) { rtpSession.SendAudioFrame(rtpSendTimestamp, sample); rtpSendTimestamp += (uint)(8000 / waveInEvent.BufferMilliseconds); } }; // 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..."); waveInEvent?.StopRecording(); audioOutEvent?.Stop(); 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(); } }