/// <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="rtpSocket">The socket we're using to send from.</param> /// <param name="rtpSendSession">Our RTP sending session.</param> /// <param name="cts">Cancellation token to stop the call.</param> private static async void SendRtp(Socket rtpSocket, RTPSession rtpSendSession, CancellationTokenSource cts) { uint bufferSize = (uint)SILENCE_SAMPLE_PERIOD * 8; // PCM transmission rate is 64kbit/s. uint rtpSamplePeriod = (uint)(1000 / SILENCE_SAMPLE_PERIOD); uint rtpSendTimestamp = 0; uint packetSentCount = 0; uint bytesSentCount = 0; while (cts.IsCancellationRequested == false) { 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; } if (_remoteRtpEndPoint != null) { rtpSendSession.SendAudioFrame(rtpSocket, _remoteRtpEndPoint, rtpSendTimestamp, sample); rtpSendTimestamp += rtpSamplePeriod; packetSentCount++; bytesSentCount += (uint)sample.Length; } await Task.Delay((int)rtpSamplePeriod); } }
/// <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="rtpChannel">The RTP channel we're sending from.</param> /// <param name="rtpSendSession">Our RTP sending session.</param> /// <param name="cts">Cancellation token to stop the call.</param> private static async void SendSilence(RTPSession rtpSession, CancellationTokenSource cts) { int samplingFrequency = rtpSession.MediaFormat.GetClockRate(); uint rtpTimestampStep = (uint)(samplingFrequency * SILENCE_SAMPLE_PERIOD / 1000); uint bufferSize = (uint)SILENCE_SAMPLE_PERIOD; uint rtpSampleTimestamp = 0; while (cts.IsCancellationRequested == false) { if (rtpSession.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; } rtpSession.SendAudioFrame(rtpSampleTimestamp, sample); rtpSampleTimestamp += rtpTimestampStep; } await Task.Delay(SILENCE_SAMPLE_PERIOD); } }
private static void SendRtp(Socket rtpSocket, RTPSession rtpSendSession, CancellationTokenSource cts) { WaveFormat waveFormat = new WaveFormat(8000, 16, 1); // The format that both the input and output audio streams will use, i.e. PCMU. // Set up the input device that will provide audio samples that can be encoded, packaged into RTP and sent to // the remote end of the call. if (WaveInEvent.DeviceCount == 0) { Log.LogWarning("No audio input devices available. No audio will be sent."); } else { DateTime lastSendReportAt = DateTime.Now; uint rtpSendTimestamp = 0; uint packetSentCount = 0; uint bytesSentCount = 0; // Device used to get audio sample from, e.g. microphone. using (WaveInEvent waveInEvent = new WaveInEvent()) { waveInEvent.BufferMilliseconds = 20; // This sets the frequency of the RTP packets. waveInEvent.NumberOfBuffers = 1; waveInEvent.DeviceNumber = 0; waveInEvent.WaveFormat = waveFormat; 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 (_remoteRtpEndPoint != null) { rtpSendSession.SendAudioFrame(rtpSocket, _remoteRtpEndPoint, rtpSendTimestamp, sample); rtpSendTimestamp += (uint)(8000 / waveInEvent.BufferMilliseconds); packetSentCount++; bytesSentCount += (uint)sample.Length; } if (DateTime.Now.Subtract(lastSendReportAt).TotalSeconds > RTP_REPORTING_PERIOD_SECONDS) { // This is typically where RTCP sender (SR) reports would be sent. Omitted here for brevity. lastSendReportAt = DateTime.Now; var remoteRtpEndPoint = _remoteRtpEndPoint as IPEndPoint; Log.LogDebug($"RTP send report {rtpSocket.LocalEndPoint}->{remoteRtpEndPoint} pkts {packetSentCount} bytes {bytesSentCount}"); } }; waveInEvent.StartRecording(); cts.Token.WaitHandle.WaitOne(); } } }
/// <summary> /// Sends two separate RTP streams to an application like ffplay. /// /// ffplay -i ffplay_av.sdp -protocol_whitelist "file,rtp,udp" -loglevel debug /// /// The SDP that describes the streams is: /// /// v=0 /// o=- 1129870806 2 IN IP4 127.0.0.1 /// s=- /// c=IN IP4 192.168.11.50 /// t=0 0 /// m=audio 4040 RTP/AVP 0 /// a=rtpmap:0 PCMU/8000 /// m=video 4042 RTP/AVP 100 /// a=rtpmap:100 VP8/90000 /// </summary> private void SendSamplesAsRtp(IPEndPoint dstBaseEndPoint) { try { Socket videoSrcRtpSocket = null; Socket videoSrcControlSocket = null; Socket audioSrcRtpSocket = null; Socket audioSrcControlSocket = null; // WebRtc multiplexes all the RTP and RTCP sessions onto a single UDP connection. // The approach needed for ffplay is the original way where each media type has it's own UDP connection and the RTCP // also require a separate UDP connection on RTP port + 1. IPAddress localIPAddress = IPAddress.Any; IPEndPoint audioRtpEP = dstBaseEndPoint; IPEndPoint audioRtcpEP = new IPEndPoint(dstBaseEndPoint.Address, dstBaseEndPoint.Port + 1); IPEndPoint videoRtpEP = new IPEndPoint(dstBaseEndPoint.Address, dstBaseEndPoint.Port + 2); IPEndPoint videoRtcpEP = new IPEndPoint(dstBaseEndPoint.Address, dstBaseEndPoint.Port + 3); RTPSession audioRtpSession = new RTPSession((int)RTPPayloadTypesEnum.PCMU, null, null); RTPSession videoRtpSession = new RTPSession(VP8_PAYLOAD_TYPE_ID, null, null); DateTime lastRtcpSenderReportSentAt = DateTime.Now; NetServices.CreateRtpSocket(localIPAddress, RAW_RTP_START_PORT_RANGE, RAW_RTP_END_PORT_RANGE, true, out audioSrcRtpSocket, out audioSrcControlSocket); NetServices.CreateRtpSocket(localIPAddress, ((IPEndPoint)audioSrcRtpSocket.LocalEndPoint).Port, RAW_RTP_END_PORT_RANGE, true, out videoSrcRtpSocket, out videoSrcControlSocket); OnMediaSampleReady += (mediaType, timestamp, sample) => { if (mediaType == MediaSampleTypeEnum.VP8) { videoRtpSession.SendVp8Frame(videoSrcRtpSocket, videoRtpEP, timestamp, sample); } else { audioRtpSession.SendAudioFrame(audioSrcRtpSocket, audioRtpEP, timestamp, sample); } // Deliver periodic RTCP sender reports. This helps the receiver to sync the audio and video stream timestamps. // If there are gaps in the media, silence supression etc. then the sender repors shouldn't be triggered from the media samples. // In this case the samples are from an mp4 file which provides a constant uninterrupted stream. if (DateTime.Now.Subtract(lastRtcpSenderReportSentAt).TotalSeconds >= RTCP_SR_PERIOD_SECONDS) { videoRtpSession.SendRtcpSenderReport(videoSrcControlSocket, videoRtcpEP, _vp8Timestamp); audioRtpSession.SendRtcpSenderReport(audioSrcControlSocket, audioRtcpEP, _mulawTimestamp); lastRtcpSenderReportSentAt = DateTime.Now; } }; } catch (Exception excp) { logger.Error("Exception SendSamplesAsRtp. " + excp); } }
/// <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="rtpSocket">The socket we're using to send from.</param> /// <param name="rtpSendSession">Our RTP sending session.</param> /// <param name="cts">Cancellation token to stop the call.</param> private static async void SendRtp(Socket rtpSocket, RTPSession rtpSendSession, CancellationTokenSource cts) { int samplingFrequency = RTPPayloadTypes.GetSamplingFrequency((RTPPayloadTypesEnum)rtpSendSession.PayloadType); uint rtpTimestampStep = (uint)(samplingFrequency * SILENCE_SAMPLE_PERIOD / 1000); uint bufferSize = (uint)SILENCE_SAMPLE_PERIOD; uint rtpSendTimestamp = 0; uint packetSentCount = 0; uint bytesSentCount = 0; while (cts.IsCancellationRequested == false) { if (_remoteRtpEndPoint != null) { if (!_dtmfEvents.IsEmpty) { // Check if there are any DTMF events to send. _dtmfEvents.TryDequeue(out var rtpEvent); if (rtpEvent != null) { await rtpSendSession.SendDtmfEvent(rtpSocket, _remoteRtpEndPoint, rtpEvent, rtpSendTimestamp, (ushort)SILENCE_SAMPLE_PERIOD, (ushort)rtpTimestampStep, cts); } rtpSendTimestamp += rtpEvent.TotalDuration + rtpTimestampStep; } else { // If there are no DTMF events to send we'll send silence. 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; } rtpSendSession.SendAudioFrame(rtpSocket, _remoteRtpEndPoint, rtpSendTimestamp, sample); rtpSendTimestamp += rtpTimestampStep; packetSentCount++; bytesSentCount += (uint)sample.Length; } } await Task.Delay(SILENCE_SAMPLE_PERIOD); } }
public void SendMedia(MediaSampleTypeEnum mediaType, uint sampleTimestamp, byte[] sample) { var connectedIceCandidate = Peer.LocalIceCandidates.Where(y => y.RemoteRtpEndPoint != null).FirstOrDefault(); if (connectedIceCandidate != null) { var srcRtpEndPoint = connectedIceCandidate.LocalRtpSocket; var dstRtpEndPoint = connectedIceCandidate.RemoteRtpEndPoint; if (mediaType == MediaSampleTypeEnum.VP8) { _videoRtpSession.SendVp8Frame(srcRtpEndPoint, dstRtpEndPoint, sampleTimestamp, sample); } else { _audioRtpSession.SendAudioFrame(srcRtpEndPoint, dstRtpEndPoint, sampleTimestamp, sample); } } }
/// <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="rtpSocket">The socket we're using to send from.</param> /// <param name="rtpSendSession">Our RTP sending session.</param> /// <param name="cts">Cancellation token to stop the call.</param> private static async void SendRtp(Socket rtpSocket, RTPSession rtpSendSession, CancellationTokenSource cts) { try { while (cts.IsCancellationRequested == false) { uint timestamp = 0; using (StreamReader sr = new StreamReader(AUDIO_FILE_PCMU)) { DateTime lastSendReportAt = DateTime.Now; uint packetsSentCount = 0; uint bytesSentCount = 0; byte[] buffer = new byte[320]; int bytesRead = sr.BaseStream.Read(buffer, 0, buffer.Length); while (bytesRead > 0 && !cts.IsCancellationRequested) { if (rtpSendSession.DestinationEndPoint != null) { packetsSentCount++; bytesSentCount += (uint)bytesRead; rtpSendSession.SendAudioFrame(rtpSocket, rtpSendSession.DestinationEndPoint, timestamp, buffer); } timestamp += (uint)buffer.Length; if (DateTime.Now.Subtract(lastSendReportAt).TotalSeconds > RTP_REPORTING_PERIOD_SECONDS) { lastSendReportAt = DateTime.Now; SIPSorcery.Sys.Log.Logger.LogDebug($"RTP send report {rtpSocket.LocalEndPoint}->{rtpSendSession.DestinationEndPoint} pkts {packetsSentCount} bytes {bytesSentCount}"); } await Task.Delay(40, cts.Token); bytesRead = sr.BaseStream.Read(buffer, 0, buffer.Length); } } } } catch (ObjectDisposedException) // Gets thrown when the RTP socket is closed. Can safely ignore. { } }
private static async Task SendRecvRtp(Socket rtpSocket, RTPSession rtpSession, IPEndPoint dstRtpEndPoint, string audioFileName, CancellationTokenSource cts) { try { SIPSorcery.Sys.Log.Logger.LogDebug($"Sending from RTP socket {rtpSocket.LocalEndPoint} to {dstRtpEndPoint}."); // Nothing is being done with the data being received from the client. But the remote rtp socket will // be switched if it differs from the one in the SDP. This helps cope with NAT. var rtpRecvTask = Task.Run(async() => { DateTime lastRecvReportAt = DateTime.Now; uint packetReceivedCount = 0; uint bytesReceivedCount = 0; byte[] buffer = new byte[512]; EndPoint remoteEP = new IPEndPoint(IPAddress.Any, 0); SIPSorcery.Sys.Log.Logger.LogDebug($"Listening on RTP socket {rtpSocket.LocalEndPoint}."); var recvResult = await rtpSocket.ReceiveFromAsync(buffer, SocketFlags.None, remoteEP); while (recvResult.ReceivedBytes > 0 && !cts.IsCancellationRequested) { RTPPacket rtpPacket = new RTPPacket(buffer.Take(recvResult.ReceivedBytes).ToArray()); packetReceivedCount++; bytesReceivedCount += (uint)rtpPacket.Payload.Length; recvResult = await rtpSocket.ReceiveFromAsync(buffer, SocketFlags.None, remoteEP); if (DateTime.Now.Subtract(lastRecvReportAt).TotalSeconds > RTP_REPORTING_PERIOD_SECONDS) { lastRecvReportAt = DateTime.Now; dstRtpEndPoint = recvResult.RemoteEndPoint as IPEndPoint; SIPSorcery.Sys.Log.Logger.LogDebug($"RTP recv {rtpSocket.LocalEndPoint}<-{dstRtpEndPoint} pkts {packetReceivedCount} bytes {bytesReceivedCount}"); } } }); string audioFileExt = Path.GetExtension(audioFileName).ToLower(); switch (audioFileExt) { case ".g722": case ".ulaw": { uint timestamp = 0; using (StreamReader sr = new StreamReader(audioFileName)) { DateTime lastSendReportAt = DateTime.Now; uint packetReceivedCount = 0; uint bytesReceivedCount = 0; byte[] buffer = new byte[320]; int bytesRead = sr.BaseStream.Read(buffer, 0, buffer.Length); while (bytesRead > 0 && !cts.IsCancellationRequested) { packetReceivedCount++; bytesReceivedCount += (uint)bytesRead; if (!dstRtpEndPoint.Address.Equals(IPAddress.Any)) { rtpSession.SendAudioFrame(rtpSocket, dstRtpEndPoint, timestamp, buffer); } timestamp += (uint)buffer.Length; if (DateTime.Now.Subtract(lastSendReportAt).TotalSeconds > RTP_REPORTING_PERIOD_SECONDS) { lastSendReportAt = DateTime.Now; SIPSorcery.Sys.Log.Logger.LogDebug($"RTP send {rtpSocket.LocalEndPoint}->{dstRtpEndPoint} pkts {packetReceivedCount} bytes {bytesReceivedCount}"); } await Task.Delay(40, cts.Token); bytesRead = sr.BaseStream.Read(buffer, 0, buffer.Length); } } } break; case ".mp3": { DateTime lastSendReportAt = DateTime.Now; uint packetReceivedCount = 0; uint bytesReceivedCount = 0; var pcmFormat = new WaveFormat(8000, 16, 1); var ulawFormat = WaveFormat.CreateMuLawFormat(8000, 1); uint timestamp = 0; using (WaveFormatConversionStream pcmStm = new WaveFormatConversionStream(pcmFormat, new Mp3FileReader(audioFileName))) { using (WaveFormatConversionStream ulawStm = new WaveFormatConversionStream(ulawFormat, pcmStm)) { byte[] buffer = new byte[320]; int bytesRead = ulawStm.Read(buffer, 0, buffer.Length); while (bytesRead > 0 && !cts.IsCancellationRequested) { packetReceivedCount++; bytesReceivedCount += (uint)bytesRead; byte[] sample = new byte[bytesRead]; Array.Copy(buffer, sample, bytesRead); if (dstRtpEndPoint.Address != IPAddress.Any) { rtpSession.SendAudioFrame(rtpSocket, dstRtpEndPoint, timestamp, buffer); } timestamp += (uint)buffer.Length; if (DateTime.Now.Subtract(lastSendReportAt).TotalSeconds > RTP_REPORTING_PERIOD_SECONDS) { lastSendReportAt = DateTime.Now; SIPSorcery.Sys.Log.Logger.LogDebug($"RTP send {rtpSocket.LocalEndPoint}->{dstRtpEndPoint} pkts {packetReceivedCount} bytes {bytesReceivedCount}"); } await Task.Delay(40, cts.Token); bytesRead = ulawStm.Read(buffer, 0, buffer.Length); } } } } break; default: throw new NotImplementedException($"The {audioFileExt} file type is not understood by this example."); } } catch (OperationCanceledException) { } catch (Exception excp) { SIPSorcery.Sys.Log.Logger.LogError($"Exception sending RTP. {excp.Message}"); } }
private static readonly int RTP_REPORTING_PERIOD_SECONDS = 5; // Period at which to write RTP stats. static void Main() { Console.WriteLine("SIPSorcery client user agent example."); Console.WriteLine("Press ctrl-c to exit."); // Plumbing code to facilitate a graceful exit. CancellationTokenSource cts = new CancellationTokenSource(); bool isCallHungup = false; bool hasCallFailed = false; // Logging configuration. Can be ommitted if internal SIPSorcery debug and warning messages are not required. var loggerFactory = new Microsoft.Extensions.Logging.LoggerFactory(); var loggerConfig = new LoggerConfiguration() .Enrich.FromLogContext() .MinimumLevel.Is(Serilog.Events.LogEventLevel.Debug) .WriteTo.Console() .CreateLogger(); loggerFactory.AddSerilog(loggerConfig); SIPSorcery.Sys.Log.LoggerFactory = loggerFactory; // Set up a default SIP transport. IPAddress defaultAddr = LocalIPConfig.GetDefaultIPv4Address(); var sipTransport = new SIPTransport(SIPDNSManager.ResolveSIPService, new SIPTransactionEngine()); int port = FreePort.FindNextAvailableUDPPort(SIPConstants.DEFAULT_SIP_PORT + 2); var sipChannel = new SIPUDPChannel(new IPEndPoint(defaultAddr, port)); sipTransport.AddSIPChannel(sipChannel); // Initialise an RTP session to receive the RTP packets from the remote SIP server. Socket rtpSocket = null; Socket controlSocket = null; NetServices.CreateRtpSocket(defaultAddr, 49000, 49100, false, out rtpSocket, out controlSocket); var rtpSendSession = new RTPSession((int)RTPPayloadTypesEnum.PCMU, null, null); // 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) => SIPSorcery.Sys.Log.Logger.LogInformation($"{uac.CallDescriptor.To} Trying: {resp.StatusCode} {resp.ReasonPhrase}."); uac.CallRinging += (uac, resp) => SIPSorcery.Sys.Log.Logger.LogInformation($"{uac.CallDescriptor.To} Ringing: {resp.StatusCode} {resp.ReasonPhrase}."); uac.CallFailed += (uac, err) => { SIPSorcery.Sys.Log.Logger.LogWarning($"{uac.CallDescriptor.To} Failed: {err}"); hasCallFailed = true; }; uac.CallAnswered += (uac, resp) => { if (resp.Status == SIPResponseStatusCodesEnum.Ok) { SIPSorcery.Sys.Log.Logger.LogInformation($"{uac.CallDescriptor.To} Answered: {resp.StatusCode} {resp.ReasonPhrase}."); IPEndPoint remoteRtpEndPoint = SDP.GetSDPRTPEndPoint(resp.Body); SIPSorcery.Sys.Log.Logger.LogDebug($"Sending initial RTP packet to remote RTP socket {remoteRtpEndPoint}."); // Send a dummy packet to open the NAT session on the RTP path. rtpSendSession.SendAudioFrame(rtpSocket, remoteRtpEndPoint, 0, new byte[] { 0x00 }); } else { SIPSorcery.Sys.Log.Logger.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 += (SIPEndPoint localSIPEndPoint, SIPEndPoint remoteEndPoint, SIPRequest sipRequest) => { if (sipRequest.Method == SIPMethodsEnum.BYE) { SIPNonInviteTransaction byeTransaction = sipTransport.CreateNonInviteTransaction(sipRequest, remoteEndPoint, localSIPEndPoint, null); SIPResponse byeResponse = SIPTransport.GetResponse(sipRequest, SIPResponseStatusCodesEnum.Ok, null); byeTransaction.SendFinalResponse(byeResponse); if (uac.IsUACAnswered) { SIPSorcery.Sys.Log.Logger.LogInformation("Call was hungup by remote server."); isCallHungup = true; cts.Cancel(); } } }; // It's a good idea to start the RTP receiving socket before the call request is sent. // A SIP server will generally start sending RTP as soon as it has processed the incoming call request and // being ready to receive will stop any ICMP error response being generated. Task.Run(() => SendRecvRtp(rtpSocket, rtpSendSession, cts)); // Start the thread that places the call. SIPCallDescriptor callDescriptor = new SIPCallDescriptor( SIPConstants.SIP_DEFAULT_USERNAME, null, DESTINATION_SIP_URI, SIPConstants.SIP_DEFAULT_FROMURI, null, null, null, null, SIPCallDirection.Out, SDP.SDP_MIME_CONTENTTYPE, GetSDP(rtpSocket.LocalEndPoint as IPEndPoint).ToString(), null); uac.Call(callDescriptor); // At this point the call has been initiated and everything will be handled in an event handler or on the RTP // receive task. The code below is to gracefully exit. // Ctrl-c will gracefully exit the call at any point. Console.CancelKeyPress += async delegate(object sender, ConsoleCancelEventArgs e) { e.Cancel = true; cts.Cancel(); SIPSorcery.Sys.Log.Logger.LogInformation("Exiting..."); rtpSocket?.Close(); controlSocket?.Close(); if (!isCallHungup && uac != null) { if (uac.IsUACAnswered) { SIPSorcery.Sys.Log.Logger.LogInformation($"Hanging up call to {uac.CallDescriptor.To}."); uac.Hangup(); } else if (!hasCallFailed) { SIPSorcery.Sys.Log.Logger.LogInformation($"Cancelling call to {uac.CallDescriptor.To}."); uac.Cancel(); } // Give the BYE or CANCEL request time to be transmitted. SIPSorcery.Sys.Log.Logger.LogInformation("Waiting 1s for call to clean up..."); await Task.Delay(1000); } SIPSorcery.Net.DNSManager.Stop(); if (sipTransport != null) { SIPSorcery.Sys.Log.Logger.LogInformation("Shutting down SIP transport..."); sipTransport.Shutdown(); } }; }
/// <summary> /// Handling packets received on the RTP socket. One of the simplest, if not the simplest, cases, is /// PCMU audio packets. THe handling can get substantially more complicated if the RTP socket is being /// used to multiplex different protocols. This is what WebRTC does with STUN, RTP and RTCP. /// </summary> /// <param name="rtpSocket">The raw RTP socket.</param> /// <param name="rtpSendSession">The session infor for the RTP pakcets being sent.</param> private static async void SendRecvRtp(Socket rtpSocket, RTPSession rtpSendSession, CancellationTokenSource cts) { try { DateTime lastRecvReportAt = DateTime.Now; uint packetReceivedCount = 0; uint bytesReceivedCount = 0; uint packetSentCount = 0; uint bytesSentCount = 0; byte[] buffer = new byte[512]; uint rtpSendTimestamp = 0; IPEndPoint anyEndPoint = new IPEndPoint(IPAddress.Any, 0); SIPSorcery.Sys.Log.Logger.LogDebug($"Listening on RTP socket {rtpSocket.LocalEndPoint}."); using (var waveOutEvent = new WaveOutEvent()) { var waveProvider = new BufferedWaveProvider(new WaveFormat(8000, 16, 1)); waveOutEvent.Init(waveProvider); waveOutEvent.Play(); var recvResult = await rtpSocket.ReceiveFromAsync(buffer, SocketFlags.None, anyEndPoint); SIPSorcery.Sys.Log.Logger.LogDebug($"Initial RTP packet recieved from {recvResult.RemoteEndPoint}."); while (recvResult.ReceivedBytes > 0 && !cts.IsCancellationRequested) { var rtpPacket = new RTPPacket(buffer.Take(recvResult.ReceivedBytes).ToArray()); packetReceivedCount++; bytesReceivedCount += (uint)rtpPacket.Payload.Length; for (int index = 0; index < rtpPacket.Payload.Length; index++) { short pcm = NAudio.Codecs.MuLawDecoder.MuLawToLinearSample(rtpPacket.Payload[index]); byte[] pcmSample = new byte[] { (byte)(pcm & 0xFF), (byte)(pcm >> 8) }; waveProvider.AddSamples(pcmSample, 0, 2); } // Periodically Send a dummy packet to keep any NAT session that may be on the media path open. if (DateTime.Now.Subtract(lastRecvReportAt).TotalSeconds > RTP_REPORTING_PERIOD_SECONDS) { // This is typically where RTCP reports would be sent. Omitted here for brevity. lastRecvReportAt = DateTime.Now; var remoteRtpEndPoint = recvResult.RemoteEndPoint as IPEndPoint; SIPSorcery.Sys.Log.Logger.LogDebug($"RTP recv {rtpSocket.LocalEndPoint}<-{remoteRtpEndPoint} pkts {packetReceivedCount} bytes {bytesReceivedCount}"); rtpSendSession.SendAudioFrame(rtpSocket, recvResult.RemoteEndPoint as IPEndPoint, rtpSendTimestamp, new byte[] { 0x00 }); rtpSendTimestamp += 32000; // Arbitrary and not critical. Corresponds to 40ms payload at 25pps which means 4s for 100 packets. packetSentCount++; bytesSentCount++; SIPSorcery.Sys.Log.Logger.LogDebug($"RTP sent {rtpSocket.LocalEndPoint}->{remoteRtpEndPoint} pkts {packetSentCount} bytes {bytesSentCount}"); } recvResult = await rtpSocket.ReceiveFromAsync(buffer, SocketFlags.None, anyEndPoint); } } } catch (ObjectDisposedException) { } // This is how .Net deals with an in use socket being closed. Safe to ignore. catch (Exception excp) { SIPSorcery.Sys.Log.Logger.LogError($"Exception processing RTP. {excp}"); } }
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. CancellationTokenSource rtpCts = new CancellationTokenSource(); // Cancellation token to stop the RTP stream. bool isCallHungup = false; bool hasCallFailed = false; // Logging configuration. Can be ommitted if internal SIPSorcery debug and warning messages are not required. var loggerFactory = new Microsoft.Extensions.Logging.LoggerFactory(); var loggerConfig = new LoggerConfiguration() .Enrich.FromLogContext() .MinimumLevel.Is(Serilog.Events.LogEventLevel.Debug) .WriteTo.Console() .CreateLogger(); loggerFactory.AddSerilog(loggerConfig); SIPSorcery.Sys.Log.LoggerFactory = loggerFactory; SIPURI callUri = SIPURI.ParseSIPURI(DEFAULT_DESTINATION_SIP_URI); if (args != null && args.Length > 0) { if (!SIPURI.TryParse(args[0])) { Log.LogWarning($"Command line argument could not be parsed as a SIP URI {args[0]}"); } else { callUri = SIPURI.ParseSIPURIRelaxed(args[0]); } } Log.LogInformation($"Call destination {callUri}."); // Set up a default SIP transport. var sipTransport = new SIPTransport(); int port = SIPConstants.DEFAULT_SIP_PORT + 1000; IPAddress localAddress = sipTransport.GetLocalAddress(IPAddress.Parse("8.8.8.8")); sipTransport.AddSIPChannel(new SIPUDPChannel(new IPEndPoint(localAddress, port))); //sipTransport.AddSIPChannel(new SIPUDPChannel(new IPEndPoint(IPAddress.Any, port))); //sipTransport.AddSIPChannel(new SIPUDPChannel(new IPEndPoint(IPAddress.IPv6Any, port))); //EnableTraceLogs(sipTransport); // Select the IP address to use for RTP based on the destination SIP URI. var endPointForCall = callUri.ToSIPEndPoint() == null?sipTransport.GetDefaultSIPEndPoint(callUri.Protocol) : sipTransport.GetDefaultSIPEndPoint(callUri.ToSIPEndPoint()); // Initialise an RTP session to receive the RTP packets from the remote SIP server. Socket rtpSocket = null; Socket controlSocket = null; // TODO (find something better): If the SIP endpoint is using 0.0.0.0 for SIP use loopback for RTP. IPAddress rtpAddress = localAddress; NetServices.CreateRtpSocket(rtpAddress, 49000, 49100, false, out rtpSocket, out controlSocket); var rtpSendSession = new RTPSession((int)RTPPayloadTypesEnum.PCMU, null, null); // 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}."); IPEndPoint remoteRtpEndPoint = SDP.GetSDPRTPEndPoint(resp.Body); Log.LogDebug($"Sending initial RTP packet to remote RTP socket {remoteRtpEndPoint}."); // Send a dummy packet to open the NAT session on the RTP path. rtpSendSession.SendAudioFrame(rtpSocket, remoteRtpEndPoint, 0, new byte[] { 0x00 }); } 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 += (SIPEndPoint localSIPEndPoint, SIPEndPoint remoteEndPoint, SIPRequest sipRequest) => { if (sipRequest.Method == SIPMethodsEnum.BYE) { SIPNonInviteTransaction byeTransaction = sipTransport.CreateNonInviteTransaction(sipRequest, remoteEndPoint, localSIPEndPoint, null); SIPResponse byeResponse = SIPTransport.GetResponse(sipRequest, SIPResponseStatusCodesEnum.Ok, null); byeTransaction.SendFinalResponse(byeResponse); if (uac.IsUACAnswered) { Log.LogInformation("Call was hungup by remote server."); isCallHungup = true; rtpCts.Cancel(); } } }; // It's a good idea to start the RTP receiving socket before the call request is sent. // A SIP server will generally start sending RTP as soon as it has processed the incoming call request and // being ready to receive will stop any ICMP error response being generated. Task.Run(() => SendRecvRtp(rtpSocket, rtpSendSession, 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, GetSDP(rtpSocket.LocalEndPoint as IPEndPoint).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(); }; // At this point the call has been initiated and everything will be handled in an event handler or on the RTP // receive task. The code below is to gracefully exit. // Wait for a signal saying the call failed, was cancelled with ctrl-c or completed. rtpCts.Token.WaitHandle.WaitOne(); Log.LogInformation("Exiting..."); rtpSocket?.Close(); controlSocket?.Close(); 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(); } }
private static async Task SendRtp(RTPSession rtpSession, IPEndPoint dstRtpEndPoint, string audioFileName, CancellationTokenSource cts) { try { string audioFileExt = Path.GetExtension(audioFileName).ToLower(); switch (audioFileExt) { case ".g722": case ".ulaw": { uint timestamp = 0; using (StreamReader sr = new StreamReader(audioFileName)) { byte[] buffer = new byte[320]; int bytesRead = sr.BaseStream.Read(buffer, 0, buffer.Length); while (bytesRead > 0 && !cts.IsCancellationRequested) { if (!dstRtpEndPoint.Address.Equals(IPAddress.Any)) { rtpSession.SendAudioFrame(timestamp, buffer); } timestamp += (uint)buffer.Length; await Task.Delay(40, cts.Token); bytesRead = sr.BaseStream.Read(buffer, 0, buffer.Length); } } } break; case ".mp3": { var pcmFormat = new WaveFormat(8000, 16, 1); var ulawFormat = WaveFormat.CreateMuLawFormat(8000, 1); uint timestamp = 0; using (WaveFormatConversionStream pcmStm = new WaveFormatConversionStream(pcmFormat, new Mp3FileReader(audioFileName))) { using (WaveFormatConversionStream ulawStm = new WaveFormatConversionStream(ulawFormat, pcmStm)) { byte[] buffer = new byte[320]; int bytesRead = ulawStm.Read(buffer, 0, buffer.Length); while (bytesRead > 0 && !cts.IsCancellationRequested) { byte[] sample = new byte[bytesRead]; Array.Copy(buffer, sample, bytesRead); if (dstRtpEndPoint.Address != IPAddress.Any) { rtpSession.SendAudioFrame(timestamp, buffer); } timestamp += (uint)buffer.Length; await Task.Delay(40, cts.Token); bytesRead = ulawStm.Read(buffer, 0, buffer.Length); } } } } break; default: throw new NotImplementedException($"The {audioFileExt} file type is not understood by this example."); } } catch (OperationCanceledException) { } catch (Exception excp) { SIPSorcery.Sys.Log.Logger.LogError($"Exception sending RTP. {excp.Message}"); } }
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 RTPSession((int)SDPMediaFormatsEnum.PCMU, null, null, true, 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(); } }