Exemplo n.º 1
0
        /// <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);
            }
        }
Exemplo n.º 2
0
        /// <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);
            }
        }
Exemplo n.º 3
0
        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();
                }
            }
        }
Exemplo n.º 4
0
        /// <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);
            }
        }
Exemplo n.º 5
0
        /// <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);
            }
        }
Exemplo n.º 6
0
        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);
                }
            }
        }
Exemplo n.º 7
0
        /// <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.
            { }
        }
Exemplo n.º 8
0
        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}");
            }
        }
Exemplo n.º 9
0
        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();
                }
            };
        }
Exemplo n.º 10
0
        /// <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}");
            }
        }
Exemplo n.º 11
0
        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();
            }
        }
Exemplo n.º 12
0
Arquivo: Utils.cs Projeto: spFly/sip
        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}");
            }
        }
Exemplo n.º 13
0
        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();
            }
        }