Rtsp lister
Inheritance: IDisposable
Exemple #1
0
        /// <summary>
        /// Add a listener.
        /// </summary>
        /// <param name="listener">A listener.</param>
        public void AddListener(RtspListener listener)
        {
            if (listener == null)
                throw new ArgumentNullException("listener");
            Contract.EndContractBlock();

            listener.MessageReceived += new EventHandler<RtspChunkEventArgs>(Listener_MessageReceived);
            _serverListener.Add(listener.RemoteAdress, listener);
        }
Exemple #2
0
        public RTSPClient()
        {
            _rtspChannel = Channel.CreateUnbounded <RtspMessage>(new UnboundedChannelOptions()
            {
                SingleReader = true,
                SingleWriter = false
            });

            rtspSocket   = new RtspTcpTransport();
            rtspListener = new RtspListener(rtspSocket, rtspErrorSubject);
            rtspListener.MessageReceived += (_, args) => _rtspChannel?.Writer.TryWrite(args.Message as RtspResponse);
            rtspListener.DataReceived    += RtpDataReceived;
            rtspListener.AutoReconnect    = false;
        }
Exemple #3
0
        public RTSPConnection(Guid connectionId, Rtsp.IRtspTransport rtspTransport, IPAddress ipAddress,
                              IRequestUrlVideoSourceResolverStrategy requestUrlVideoSourceResolverStrategy)
        {
            Id = connectionId;

            _ssrc = GLOBAL_SSRC;
            _timeSinceLastRtspKeepAlive = DateTime.UtcNow;
            _timeSinceLastRtcpKeepAlive = DateTime.UtcNow;
            _ipAddress = ipAddress;
            _requestUrlVideoSourceResolverStrategy = requestUrlVideoSourceResolverStrategy;

            _listener                        = new Rtsp.RtspListener(rtspTransport, "RtspServer", Guid.Empty, Id);
            _clientHostname                  = _listener.RemoteAdress.Split(':')[0];
            _listener.MessageReceived       += RTSP_Message_Received;
            _listener.SocketExceptionRaised += RTSP_SocketException_Raised;
            _listener.Disconnected          += RTSP_Disconnected;

            _logger.Info($"Connection {Id} opened. Client: {rtspTransport.RemoteAddress}");
        }
Exemple #4
0
        // Process each RTSP message that is received
        private void RTSP_Message_Received(object sender, RtspChunkEventArgs e)
        {
            // Cast the 'sender' and 'e' into the RTSP Listener (the Socket) and the RTSP Message
            Rtsp.RtspListener         listener = sender as Rtsp.RtspListener;
            Rtsp.Messages.RtspMessage message  = e.Message as Rtsp.Messages.RtspMessage;

            Console.WriteLine("RTSP message received " + message);

            // Update the RTSP Keepalive Timeout
            // We could check that the message is GET_PARAMETER or OPTIONS for a keepalive but instead we will update the timer on any message
            lock (rtsp_list)
            {
                foreach (RTSPConnection connection in rtsp_list)
                {
                    if (connection.listener.RemoteAdress.Equals(listener.RemoteAdress))
                    {
                        // found the connection
                        connection.time_since_last_rtsp_keepalive = DateTime.UtcNow;
                        break;
                    }
                }
            }

            #region Handle OPTIONS message
            if (message is Rtsp.Messages.RtspRequestOptions)
            {
                // Create the reponse to OPTIONS
                Rtsp.Messages.RtspResponse options_response =
                    (e.Message as Rtsp.Messages.RtspRequestOptions).CreateResponse("DESCRIBE,SETUP,PLAY,TEARDOWN");
                listener.SendMessage(options_response);
            }
            #endregion

            #region Handle DESCRIBE message
            if (message is Rtsp.Messages.RtspRequestDescribe)
            {
                String requested_url = (message as Rtsp.Messages.RtspRequestDescribe).RtspUri.ToString();
                Console.WriteLine("Request for " + requested_url);

                // TODO. Check the requsted_url is valid. In this example we accept any RTSP URL

                // Make the Base64 SPS and PPS
                String sps_str = Convert.ToBase64String(StreamInfo.SPS);
                String pps_str = Convert.ToBase64String(StreamInfo.PPS);

                StringBuilder sdp = new StringBuilder();

                // Generate the SDP
                // The sprop-parameter-sets provide the SPS and PPS for H264 video
                // The packetization-mode defines the H264 over RTP payloads used but is Optional
                sdp.Append("v=0\n");
                sdp.Append("o=user 123 0 IN IP4 0.0.0.0\n");
                sdp.Append($"s=SysDVR - https://github.com/exelix11/sysdvr - [PID {Process.GetCurrentProcess().Id}]\n");
                if (video_source != null)
                {
                    sdp.Append("m=video 0 RTP/AVP 96\n");
                    sdp.Append("c=IN IP4 0.0.0.0\n");
                    sdp.Append("a=control:trackID=0\n");
                    sdp.Append("a=rtpmap:96 H264/90000\n");
                    sdp.Append("a=fmtp:96 profile-level-id=42A01E; sprop-parameter-sets=" + sps_str + "," + pps_str + ";\n");
                }
                if (audio_source != null)
                {
                    sdp.Append("m=audio 0 RTP/AVP 97\n");
                    sdp.Append("a=rtpmap:97 L16/48000/2\n");
                    sdp.Append("a=control:trackID=1\n");
                }

                byte[] sdp_bytes = Encoding.ASCII.GetBytes(sdp.ToString());

                // Create the reponse to DESCRIBE
                // This must include the Session Description Protocol (SDP)
                Rtsp.Messages.RtspResponse describe_response = (e.Message as Rtsp.Messages.RtspRequestDescribe).CreateResponse();

                describe_response.AddHeader("Content-Base: " + requested_url);
                describe_response.AddHeader("Content-Type: application/sdp");
                describe_response.Data = sdp_bytes;
                describe_response.AdjustContentLength();
                listener.SendMessage(describe_response);
            }
            #endregion

            #region Handle SETUP message
            if (message is Rtsp.Messages.RtspRequestSetup)
            {
                var setupMessage = message as Rtsp.Messages.RtspRequestSetup;

                // Check the RTSP transport
                // If it is UDP or Multicast, create the sockets
                // If it is RTP over RTSP we send data via the RTSP Listener

                // FIXME client may send more than one possible transport.
                // very rare
                Rtsp.Messages.RtspTransport transport = setupMessage.GetTransports()[0];

                // Construct the Transport: reply from the Server to the client
                Rtsp.Messages.RtspTransport transport_reply = new Rtsp.Messages.RtspTransport();
                transport_reply.SSrc = global_ssrc.ToString("X8");                 // Convert to Hex, padded to 8 characters

                if (transport.LowerTransport == Rtsp.Messages.RtspTransport.LowerTransportType.TCP)
                {
                    // RTP over RTSP mode
                    transport_reply.LowerTransport = Rtsp.Messages.RtspTransport.LowerTransportType.TCP;
                    transport_reply.Interleaved    = new Rtsp.Messages.PortCouple(transport.Interleaved.First, transport.Interleaved.Second);
                }

                Rtsp.UDPSocket udp_pair = null;
                if (transport.LowerTransport == Rtsp.Messages.RtspTransport.LowerTransportType.UDP &&
                    transport.IsMulticast == false)
                {
                    Boolean udp_supported = true;
                    if (udp_supported)
                    {
                        // RTP over UDP mode
                        // Create a pair of UDP sockets - One is for the Video, one is for the RTCP
                        udp_pair = new Rtsp.UDPSocket(50000, 51000);                         // give a range of 500 pairs (1000 addresses) to try incase some address are in use
                        udp_pair.DataReceived += (object local_sender, RtspChunkEventArgs local_e) =>
                        {
                            // RTCP data received
                            Console.WriteLine("RTCP data received " + local_sender.ToString() + " " + local_e.ToString());
                        };
                        udp_pair.Start();                         // start listening for data on the UDP ports

                        // Pass the Port of the two sockets back in the reply
                        transport_reply.LowerTransport = Rtsp.Messages.RtspTransport.LowerTransportType.UDP;
                        transport_reply.IsMulticast    = false;
                        transport_reply.ClientPort     = new Rtsp.Messages.PortCouple(udp_pair.data_port, udp_pair.control_port);
                    }
                    else
                    {
                        transport_reply = null;
                    }
                }

                if (transport.LowerTransport == Rtsp.Messages.RtspTransport.LowerTransportType.UDP &&
                    transport.IsMulticast == true)
                {
                    // RTP over Multicast UDP mode}
                    // Create a pair of UDP sockets in Multicast Mode
                    // Pass the Ports of the two sockets back in the reply
                    transport_reply.LowerTransport = Rtsp.Messages.RtspTransport.LowerTransportType.UDP;
                    transport_reply.IsMulticast    = true;
                    transport_reply.Port           = new Rtsp.Messages.PortCouple(7000, 7001);            // FIX

                    // for now until implemented
                    transport_reply = null;
                }


                if (transport_reply != null)
                {
                    // Update the session with transport information
                    String copy_of_session_id = "";
                    lock (rtsp_list)
                    {
                        foreach (RTSPConnection connection in rtsp_list)
                        {
                            if (connection.listener.RemoteAdress.Equals(listener.RemoteAdress))
                            {
                                var stream = int.Parse(setupMessage.RtspUri.LocalPath.Split('=')[1]);
                                connection.sessions[stream].is_active = true;
                                // found the connection
                                // Add the transports to the connection
                                connection.sessions[stream].client_transport = transport;
                                connection.sessions[stream].transport_reply  = transport_reply;

                                // If we are sending in UDP mode, add the UDP Socket pair and the Client Hostname
                                connection.sessions[stream].udp_pair = udp_pair;

                                connection.sessions[stream].session_id = session_handle.ToString();
                                session_handle++;

                                // Copy the Session ID
                                copy_of_session_id = connection.sessions[stream].session_id;
                                break;
                            }
                        }
                    }

                    Rtsp.Messages.RtspResponse setup_response = setupMessage.CreateResponse();
                    setup_response.Headers[Rtsp.Messages.RtspHeaderNames.Transport] = transport_reply.ToString();
                    setup_response.Session = copy_of_session_id;
                    listener.SendMessage(setup_response);
                }
                else
                {
                    Rtsp.Messages.RtspResponse setup_response = setupMessage.CreateResponse();
                    // unsuported transport
                    setup_response.ReturnCode = 461;
                    listener.SendMessage(setup_response);
                }
            }
            #endregion

            #region Handle PLAY message
            // Must have a Session ID
            if (message is Rtsp.Messages.RtspRequestPlay)
            {
                lock (rtsp_list)
                {
                    // Search for the Session in the Sessions List. Change the state to "PLAY"
                    bool session_found = false;
                    foreach (RTSPConnection connection in rtsp_list)
                    {
                        if (message.Session == connection.video.session_id || message.Session == connection.audio.session_id)
                        {
                            // found the session
                            session_found   = true;
                            connection.play = true;                                                                                                   // ACTUALLY YOU COULD PAUSE JUST THE VIDEO (or JUST THE AUDIO)

                            string range    = "npt=0-";                                                                                               // Playing the 'video' from 0 seconds until the end
                            string rtp_info = "url=" + ((Rtsp.Messages.RtspRequestPlay)message).RtspUri + ";seq=" + connection.video.sequence_number; // TODO Add rtptime  +";rtptime="+session.rtp_initial_timestamp;

                            // Send the reply
                            Rtsp.Messages.RtspResponse play_response = (e.Message as Rtsp.Messages.RtspRequestPlay).CreateResponse();
                            play_response.AddHeader("Range: " + range);
                            play_response.AddHeader("RTP-Info: " + rtp_info);
                            listener.SendMessage(play_response);

                            break;
                        }
                    }

                    if (session_found == false)
                    {
                        // Session ID was not found in the list of Sessions. Send a 454 error
                        Rtsp.Messages.RtspResponse play_failed_response = (e.Message as Rtsp.Messages.RtspRequestPlay).CreateResponse();
                        play_failed_response.ReturnCode = 454;                         // Session Not Found
                        listener.SendMessage(play_failed_response);
                    }
                }
            }
            #endregion

            #region Handle TEARDOWN
            // Must have a Session ID
            if (message is Rtsp.Messages.RtspRequestTeardown)
            {
                lock (rtsp_list)
                {
                    // Search for the Session in the Sessions List.
                    foreach (RTSPConnection connection in rtsp_list.ToArray())                     // Convert to ToArray so we can delete from the rtp_list
                    {
                        rtsp_list.Remove(connection);
                        connection.Dispose();
                        listener.Dispose();
                    }
                }
            }
            #endregion
        }
Exemple #5
0
        /// <summary>
        /// Handles a request setup.
        /// </summary>
        /// <param name="destination">The destination.</param>
        /// <param name="requestSetup">The request setup.</param>
        /// <returns>The rewritten message</returns>
        /// <remarks>
        /// The destination can be modified.
        /// </remarks>
        private RtspMessage HandleRequestSetup(ref RtspListener destination, RtspRequestSetup requestSetup)
        {
            Contract.Requires(requestSetup != null);
            Contract.Requires(destination != null);
            Contract.Ensures(Contract.Result<RtspMessage>() != null);
            Contract.Ensures(Contract.ValueAtReturn(out destination) != null);

            // look if we already have a multicast streaming playing for this URI.
            foreach (var session in _activesSession.Values)
            {
                if (session.State == RtspSession.SessionState.Playing && session.ListOfForwader.ContainsKey(requestSetup.RtspUri))
                {
                    Forwarder existingForwarder = session.ListOfForwader[requestSetup.RtspUri];
                    if (existingForwarder != null && existingForwarder.ToMulticast)
                    {
                        RtspResponse returnValue = requestSetup.CreateResponse();
                        returnValue.Headers[RtspHeaderNames.Transport] = new RtspTransport()
                        {
                            IsMulticast = true,
                            Destination = existingForwarder.ForwardHostVideo,
                            Port = new PortCouple(existingForwarder.ForwardPortVideo, existingForwarder.ListenCommandPort),

                        }.ToString();
                        returnValue.Session = session.Name;
                        destination = requestSetup.SourcePort;
                        return returnValue;
                    }
                }
            }

            string setupKey = requestSetup.SourcePort.RemoteAdress + "SEQ" + requestSetup.CSeq.ToString(CultureInfo.InvariantCulture);

            RtspTransport selectedTransport = SelectTransport(requestSetup);

            // We do not handle asked transport so return directly.
            if (selectedTransport == null)
            {
                _logger.Info("No transport asked are supported, sorry");
                RtspResponse returnValue = requestSetup.CreateResponse();
                // Unsupported transport;
                returnValue.ReturnCode = 461;
                destination = requestSetup.SourcePort;
                return returnValue;
            }

            UDPForwarder forwarder = new UDPForwarder();
            forwarder.ToMulticast = selectedTransport.IsMulticast;

            // this part of config is only valid in unicast.
            if (!selectedTransport.IsMulticast)
            {
                forwarder.ForwardPortVideo = selectedTransport.ClientPort.First;
                forwarder.SourcePortCommand = selectedTransport.ClientPort.Second;

                // If the client did not set the destination.. get it from TCP source
                if (!string.IsNullOrEmpty(selectedTransport.Destination))
                {
                    forwarder.ForwardHostVideo = selectedTransport.Destination;
                }
                else
                {
                    forwarder.ForwardHostVideo = requestSetup.SourcePort.RemoteAdress.Split(':')[0];
                    _logger.Debug("Destination get from TCP port {0}", forwarder.ForwardHostVideo);
                }
            }

            // Configured the transport asked.
            forwarder.ForwardHostCommand = destination.RemoteAdress.Split(':')[0];
            RtspTransport firstNewTransport = new RtspTransport()
                {
                    IsMulticast = false,
                    ClientPort = new PortCouple(forwarder.ListenVideoPort, forwarder.FromForwardCommandPort),
                };

            RtspTransport secondTransport = new RtspTransport()
                {
                    IsMulticast = false,
                    LowerTransport = RtspTransport.LowerTransportType.TCP,
                };

            requestSetup.Headers[RtspHeaderNames.Transport] = firstNewTransport.ToString() + ", " + secondTransport.ToString();
            _setupForwarder.Add(setupKey, forwarder);

            return requestSetup;
        }
Exemple #6
0
        /// <summary>
        /// Gets the RTSP listener for destination.
        /// </summary>
        /// <param name="destinationUri">The destination URI.</param>
        /// <returns>An RTSP listener</returns>
        /// <remarks>
        /// This method try to get one of openned TCP listener and
        /// if it does not find it, it create it. 
        /// </remarks>
        private RtspListener GetRtspListenerForDestination(Uri destinationUri)
        {
            Contract.Requires(destinationUri != null);

            RtspListener destination;
            string destinationName = destinationUri.Authority;
            if (_serverListener.ContainsKey(destinationName))
                destination = _serverListener[destinationName];
            else
            {
                destination = new RtspListener(
                    new RtspTcpTransport(destinationUri.Host, destinationUri.Port)
                    );

                // un peu pourri mais pas d'autre idée...
                // pour avoir vraiment des clef avec IP....
                if (_serverListener.ContainsKey(destination.RemoteAdress))
                    destination = _serverListener[destination.RemoteAdress];
                else
                {
                    AddListener(destination);
                    destination.Start();
                }
            }
            return destination;
        }
Exemple #7
0
        // Process each RTSP message that is received
        private void RTSP_Message_Received(object sender, RtspChunkEventArgs e)
        {
            // Cast the 'sender' and 'e' into the RTSP Listener (the Socket) and the RTSP Message
            Rtsp.RtspListener         listener = sender as Rtsp.RtspListener;
            Rtsp.Messages.RtspMessage message  = e.Message as Rtsp.Messages.RtspMessage;

            _logger.Debug($"{listener.ConnectionId} RTSP message received " + message);


            // Check if the RTSP Message has valid authentication (validating against username,password,realm and nonce)
            if (_auth != null)
            {
                RTSP_ProcessAuthorization(message as RtspRequest, listener);
            }


            // Update the RTSP Keepalive Timeout
            // We could check that the message is GET_PARAMETER or OPTIONS for a keepalive but instead we will update the timer on any message
            _timeSinceLastRtcpKeepAlive = DateTime.UtcNow;



            // Handle OPTIONS message
            if (message is Rtsp.Messages.RtspRequestOptions)
            {
                RTSP_ProcessOptionsRequest(message as RtspRequestOptions, listener);
            }

            // Handle DESCRIBE message
            if (message is Rtsp.Messages.RtspRequestDescribe)
            {
                RTSP_ProcessDescribeRequest(message as RtspRequestDescribe, listener);
            }

            // Handle SETUP message
            if (message is Rtsp.Messages.RtspRequestSetup)
            {
                RTSP_ProcessSetupRequest(message as RtspRequestSetup, listener);
            }

            // Handle PLAY message (Sent with a Session ID)
            if (message is Rtsp.Messages.RtspRequestPlay)
            {
                RTSP_ProcessPlayRequest(message as RtspRequestPlay, listener);
            }

            // Handle PAUSE message (Sent with a Session ID)
            if (message is Rtsp.Messages.RtspRequestPause)
            {
                RTSP_ProcessPauseRequest(message as RtspRequestPause, listener);
            }


            // Handle GET_PARAMETER message, often used as a Keep Alive
            if (message is Rtsp.Messages.RtspRequestGetParameter)
            {
                RTSP_ProcessGetParameterRequest(message as RtspRequestGetParameter, listener);
            }


            // Handle TEARDOWN (sent with a Session ID)
            if (message is Rtsp.Messages.RtspRequestTeardown)
            {
                RTSP_ProcessTeardownRequest(message as RtspRequestTeardown, listener);
            }
        }
Exemple #8
0
    // Process each RTSP message that is received
    private void RTSP_Message_Received(object sender, RtspChunkEventArgs e)
    {
        // Cast the 'sender' and 'e' into the RTSP Listener (the Socket) and the RTSP Message
        Rtsp.RtspListener         listener = sender as Rtsp.RtspListener;
        Rtsp.Messages.RtspMessage message  = e.Message as Rtsp.Messages.RtspMessage;

        Console.WriteLine("RTSP message received " + message);


        // Check if the RTSP Message has valid authentication (validating against username,password,realm and nonce)
        if (auth != null)
        {
            bool authorized = false;
            if (message.Headers.ContainsKey("Authorization") == true)
            {
                // Check the message has the correct Authorization
                authorized = auth.IsValid(message);
            }
            if ((message.Headers.ContainsKey("Authorization") == false) ||
                authorized == false)
            {
                // Send a 401 Authentication Required reply
                Rtsp.Messages.RtspResponse authorization_response = (e.Message as Rtsp.Messages.RtspRequest).CreateResponse();
                authorization_response.AddHeader("WWW-Authenticate: " + auth.GetHeader());
                authorization_response.ReturnCode = 401;
                listener.SendMessage(authorization_response);
                return;
            }
        }

        // Update the RTSP Keepalive Timeout
        // We could check that the message is GET_PARAMETER or OPTIONS for a keepalive but instead we will update the timer on any message
        lock (rtsp_list)
        {
            foreach (RTSPConnection connection in rtsp_list)
            {
                if (connection.listener.RemoteAdress.Equals(listener.RemoteAdress))
                {
                    // found the connection
                    connection.time_since_last_rtsp_keepalive = DateTime.UtcNow;
                    break;
                }
            }
        }


        // Handle OPTIONS message
        if (message is Rtsp.Messages.RtspRequestOptions)
        {
            // Create the reponse to OPTIONS
            Rtsp.Messages.RtspResponse options_response = (e.Message as Rtsp.Messages.RtspRequestOptions).CreateResponse();
            listener.SendMessage(options_response);
        }

        // Handle DESCRIBE message
        if (message is Rtsp.Messages.RtspRequestDescribe)
        {
            String requested_url = (message as Rtsp.Messages.RtspRequestDescribe).RtspUri.ToString();
            Console.WriteLine("Request for " + requested_url);

            // TODO. Check the requsted_url is valid. In this example we accept any RTSP URL

            // Make the Base64 SPS and PPS
            byte[] raw_sps = h264_encoder.GetRawSPS(); // no 0x00 0x00 0x00 0x01 or 32 bit size header
            byte[] raw_pps = h264_encoder.GetRawPPS(); // no 0x00 0x00 0x00 0x01 or 32 bit size header
            String sps_str = Convert.ToBase64String(raw_sps);
            String pps_str = Convert.ToBase64String(raw_pps);

            StringBuilder sdp = new StringBuilder();

            // Generate the SDP
            // The sprop-parameter-sets provide the SPS and PPS for H264 video
            // The packetization-mode defines the H264 over RTP payloads used but is Optional
            sdp.Append("v=0\n");
            sdp.Append("o=user 123 0 IN IP4 0.0.0.0\n");
            sdp.Append("s=SharpRTSP Test Camera\n");
            sdp.Append("m=video 0 RTP/AVP 96\n");
            sdp.Append("c=IN IP4 0.0.0.0\n");
            sdp.Append("a=control:trackID=0\n");
            sdp.Append("a=rtpmap:96 H264/90000\n");
            sdp.Append("a=fmtp:96 profile-level-id=42A01E; sprop-parameter-sets=" + sps_str + "," + pps_str + ";\n");

            byte[] sdp_bytes = Encoding.ASCII.GetBytes(sdp.ToString());

            // Create the reponse to DESCRIBE
            // This must include the Session Description Protocol (SDP)
            Rtsp.Messages.RtspResponse describe_response = (e.Message as Rtsp.Messages.RtspRequestDescribe).CreateResponse();

            describe_response.AddHeader("Content-Base: " + requested_url);
            describe_response.AddHeader("Content-Type: application/sdp");
            describe_response.Data = sdp_bytes;
            describe_response.AdjustContentLength();
            listener.SendMessage(describe_response);
        }

        // Handle SETUP message
        if (message is Rtsp.Messages.RtspRequestSetup)
        {
            //
            var setupMessage = message as Rtsp.Messages.RtspRequestSetup;

            // Check the RTSP transport
            // If it is UDP or Multicast, create the sockets
            // If it is RTP over RTSP we send data via the RTSP Listener

            // FIXME client may send more than one possible transport.
            // very rare
            Rtsp.Messages.RtspTransport transport = setupMessage.GetTransports()[0];


            // Construct the Transport: reply from the Server to the client
            Rtsp.Messages.RtspTransport transport_reply = new Rtsp.Messages.RtspTransport();
            transport_reply.SSrc = global_ssrc.ToString("X8"); // Convert to Hex, padded to 8 characters

            if (transport.LowerTransport == Rtsp.Messages.RtspTransport.LowerTransportType.TCP)
            {
                // RTP over RTSP mode}
                transport_reply.LowerTransport = Rtsp.Messages.RtspTransport.LowerTransportType.TCP;
                transport_reply.Interleaved    = new Rtsp.Messages.PortCouple(transport.Interleaved.First, transport.Interleaved.Second);
            }

            Rtsp.UDPSocket udp_pair = null;
            if (transport.LowerTransport == Rtsp.Messages.RtspTransport.LowerTransportType.UDP &&
                transport.IsMulticast == false)
            {
                Boolean udp_supported = true;
                if (udp_supported)
                {
                    // RTP over UDP mode
                    // Create a pair of UDP sockets - One is for the Video, one is for the RTCP
                    udp_pair = new Rtsp.UDPSocket(50000, 51000); // give a range of 500 pairs (1000 addresses) to try incase some address are in use
                    udp_pair.DataReceived += (object local_sender, RtspChunkEventArgs local_e) => {
                        // RTCP data received
                        Console.WriteLine("RTCP data received " + local_sender.ToString() + " " + local_e.ToString());
                    };
                    udp_pair.Start(); // start listening for data on the UDP ports

                    // Pass the Port of the two sockets back in the reply
                    transport_reply.LowerTransport = Rtsp.Messages.RtspTransport.LowerTransportType.UDP;
                    transport_reply.IsMulticast    = false;
                    transport_reply.ClientPort     = new Rtsp.Messages.PortCouple(udp_pair.data_port, udp_pair.control_port);
                }
                else
                {
                    transport_reply = null;
                }
            }

            if (transport.LowerTransport == Rtsp.Messages.RtspTransport.LowerTransportType.UDP &&
                transport.IsMulticast == true)
            {
                // RTP over Multicast UDP mode}
                // Create a pair of UDP sockets in Multicast Mode
                // Pass the Ports of the two sockets back in the reply
                transport_reply.LowerTransport = Rtsp.Messages.RtspTransport.LowerTransportType.UDP;
                transport_reply.IsMulticast    = true;
                transport_reply.Port           = new Rtsp.Messages.PortCouple(7000, 7001); // FIX

                // for now until implemented
                transport_reply = null;
            }


            if (transport_reply != null)
            {
                // Update the session with transport information
                String copy_of_session_id = "";
                lock (rtsp_list)
                {
                    foreach (RTSPConnection connection in rtsp_list)
                    {
                        if (connection.listener.RemoteAdress.Equals(listener.RemoteAdress))
                        {
                            // ToDo - Check the Track ID to determine if this is a SETUP for the Video Stream
                            // or a SETUP for an Audio Stream.
                            // In the SDP the H264 video track is TrackID 0


                            // found the connection
                            // Add the transports to the connection
                            connection.video_client_transport = transport;
                            connection.video_transport_reply  = transport_reply;

                            // If we are sending in UDP mode, add the UDP Socket pair and the Client Hostname
                            connection.video_udp_pair = udp_pair;


                            connection.video_session_id = session_handle.ToString();
                            session_handle++;


                            // Copy the Session ID
                            copy_of_session_id = connection.video_session_id;
                            break;
                        }
                    }
                }

                Rtsp.Messages.RtspResponse setup_response = setupMessage.CreateResponse();
                setup_response.Headers[Rtsp.Messages.RtspHeaderNames.Transport] = transport_reply.ToString();
                setup_response.Session = copy_of_session_id;
                listener.SendMessage(setup_response);
            }
            else
            {
                Rtsp.Messages.RtspResponse setup_response = setupMessage.CreateResponse();
                // unsuported transport
                setup_response.ReturnCode = 461;
                listener.SendMessage(setup_response);
            }
        }

        // Handle PLAY message (Sent with a Session ID)
        if (message is Rtsp.Messages.RtspRequestPlay)
        {
            lock (rtsp_list)
            {
                // Search for the Session in the Sessions List. Change the state to "PLAY"
                bool session_found = false;
                foreach (RTSPConnection connection in rtsp_list)
                {
                    if (message.Session == connection.video_session_id) /* OR AUDIO_SESSION_ID */
                    {
                        // found the session
                        session_found   = true;
                        connection.play = true;                                                                                                   // ACTUALLY YOU COULD PAUSE JUST THE VIDEO (or JUST THE AUDIO)

                        string range    = "npt=0-";                                                                                               // Playing the 'video' from 0 seconds until the end
                        string rtp_info = "url=" + ((Rtsp.Messages.RtspRequestPlay)message).RtspUri + ";seq=" + connection.video_sequence_number; // TODO Add rtptime  +";rtptime="+session.rtp_initial_timestamp;

                        // Send the reply
                        Rtsp.Messages.RtspResponse play_response = (e.Message as Rtsp.Messages.RtspRequestPlay).CreateResponse();
                        play_response.AddHeader("Range: " + range);
                        play_response.AddHeader("RTP-Info: " + rtp_info);
                        listener.SendMessage(play_response);

                        break;
                    }
                }

                if (session_found == false)
                {
                    // Session ID was not found in the list of Sessions. Send a 454 error
                    Rtsp.Messages.RtspResponse play_failed_response = (e.Message as Rtsp.Messages.RtspRequestPlay).CreateResponse();
                    play_failed_response.ReturnCode = 454; // Session Not Found
                    listener.SendMessage(play_failed_response);
                }
            }
        }

        // Handle PAUSE message (Sent with a Session ID)
        if (message is Rtsp.Messages.RtspRequestPause)
        {
            lock (rtsp_list)
            {
                // Search for the Session in the Sessions List. Change the state of "PLAY"
                foreach (RTSPConnection connection in rtsp_list)
                {
                    if (message.Session == connection.video_session_id /* OR AUDIO SESSION ID */)
                    {
                        // found the session
                        connection.play = false; // COULD HAVE PLAY/PAUSE FOR VIDEO AND AUDIO
                        break;
                    }
                }
            }

            // ToDo - only send back the OK response if the Session in the RTSP message was found
            Rtsp.Messages.RtspResponse pause_response = (e.Message as Rtsp.Messages.RtspRequestPause).CreateResponse();
            listener.SendMessage(pause_response);
        }


        // Handle GET_PARAMETER message, often used as a Keep Alive
        if (message is Rtsp.Messages.RtspRequestGetParameter)
        {
            // Create the reponse to GET_PARAMETER
            Rtsp.Messages.RtspResponse getparameter_response = (e.Message as Rtsp.Messages.RtspRequestGetParameter).CreateResponse();
            listener.SendMessage(getparameter_response);
        }


        // Handle TEARDOWN (sent with a Session ID)
        if (message is Rtsp.Messages.RtspRequestTeardown)
        {
            lock (rtsp_list)
            {
                // Search for the Session in the Sessions List.
                foreach (RTSPConnection connection in rtsp_list.ToArray()) // Convert to ToArray so we can delete from the rtp_list
                {
                    if (message.Session == connection.video_session_id)    // SHOULD HAVE AN AUDIO TEARDOWN AS WELL
                    {
                        // If this is UDP, close the transport
                        // For TCP there is no transport to close (as RTP packets were interleaved into the RTSP connection)
                        if (connection.video_udp_pair != null)
                        {
                            connection.video_udp_pair.Stop();
                            connection.video_udp_pair = null;
                        }

                        rtsp_list.Remove(connection);

                        // Close the RTSP socket
                        listener.Dispose();
                    }
                }
            }
        }
    }
Exemple #9
0
    // Process each RTSP message that is received
    private async System.Threading.Tasks.Task RTSP_Message_ReceivedAsync(object sender, RtspChunkEventArgs e)
    {
        // Cast the 'sender' and 'e' into the RTSP Listener (the Socket) and the RTSP Message
        Rtsp.RtspListener         listener = sender as Rtsp.RtspListener;
        Rtsp.Messages.RtspMessage message  = e.Message as Rtsp.Messages.RtspMessage;

        Console.WriteLine("RTSP message received " + message);
        var deviceId      = "";
        var streamId      = "";
        var unixTimestamp = 0;
        var startTime     = new DateTime(1970, 1, 1);

        if (message is RtspRequest)
        {
            var rtspParameters = HttpUtility.ParseQueryString(((RtspRequest)message).RtspUri.Query);
            deviceId = rtspParameters["deviceId"];
            streamId = rtspParameters["streamId"];

            int.TryParse(rtspParameters["unixTimestamp"], out unixTimestamp);
            startTime = startTime.AddSeconds(unixTimestamp);
            Console.WriteLine($"{rtspParameters["deviceId"]}, {rtspParameters["streamId"]},{rtspParameters["unixTimestamp"]}");
        }
        if (String.IsNullOrEmpty(deviceId))
        {
            _logger.Error("No deviceId");
            return;
        }
        List <RTSPConnection> rtsp_list = new List <RTSPConnection>();

        rtsp_list = _rtspList.GetOrAdd(deviceId, rtsp_list);
        // Check if the RTSP Message has valid authentication (validating against username,password,realm and nonce)
        bool           authorized = false;
        Authentication authInfo   = null;

        if (message.Headers.ContainsKey("Authorization") == true)
        {
            // The Header contained Authorization
            // Check the message has the correct Authorization
            // If it does not have the correct Authorization then close the RTSP connection
            authInfo = Authentication.GetAuthenticationInfo(message);
            URLCommand nvrCmd        = new URLCommand(_nvrIp, uint.Parse(_nvrPort), authInfo.Username, authInfo.Password);
            string     loginResponse = null;
            authorized = nvrCmd.Login(ref loginResponse);

            if (authorized == false)
            {
                // Send a 401 Authentication Failed reply, then close the RTSP Socket
                Rtsp.Messages.RtspResponse authorization_response = (e.Message as Rtsp.Messages.RtspRequest).CreateResponse();
                authorization_response.AddHeader("WWW-Authenticate: " + auth.GetHeader());
                authorization_response.ReturnCode = 401;
                listener.SendMessage(authorization_response);

                lock (rtsp_list)
                {
                    foreach (RTSPConnection connection in rtsp_list.ToArray())
                    {
                        if (connection.listener == listener)
                        {
                            rtsp_list.Remove(connection);
                        }
                    }
                }
                listener.Dispose();
                return;
            }
            else
            {
                lock (rtsp_list)
                {
                    if (!rtsp_list.Any(rtsp => rtsp.listener.RemoteAdress == listener.RemoteAdress))
                    {
                        RTSPConnection new_connection = new RTSPConnection();
                        new_connection.listener        = listener;
                        new_connection.client_hostname = listener.RemoteAdress.Split(':')[0];
                        new_connection.ssrc            = global_ssrc;
                        new_connection.time_since_last_rtsp_keepalive       = DateTime.UtcNow;
                        new_connection.video_time_since_last_rtcp_keepalive = DateTime.UtcNow;

                        rtsp_list.Add(new_connection);
                    }
                }
                _logger.Info($"Login NVR success:{loginResponse}");
            }
        }
        else
        {
            Rtsp.Messages.RtspResponse authorization_response = (e.Message as Rtsp.Messages.RtspRequest).CreateResponse();
            authorization_response.AddHeader("WWW-Authenticate: " + auth.GetHeader()); // 'Basic' or 'Digest'
            authorization_response.ReturnCode = 401;
            listener.SendMessage(authorization_response);
            return;
        }

        // Update the RTSP Keepalive Timeout
        // We could check that the message is GET_PARAMETER or OPTIONS for a keepalive but instead we will update the timer on any message
        lock (rtsp_list)
        {
            foreach (RTSPConnection connection in rtsp_list)
            {
                if (connection.listener.RemoteAdress.Equals(listener.RemoteAdress))
                {
                    // found the connection
                    connection.time_since_last_rtsp_keepalive = DateTime.UtcNow;
                    break;
                }
            }
        }


        // Handle OPTIONS message
        if (message is Rtsp.Messages.RtspRequestOptions)
        {
            // Create the reponse to OPTIONS
            Rtsp.Messages.RtspResponse options_response = (e.Message as Rtsp.Messages.RtspRequestOptions).CreateResponse();
            listener.SendMessage(options_response);
            // parse and get deviceId from url

            if (!_nvrPlayerList.ContainsKey(deviceId))
            {
                URLCommand nvrCmd          = new URLCommand(_nvrIp, uint.Parse(_nvrPort), authInfo.Username, authInfo.Password);
                string     deviceConfigXml = null;
                //DeviceConfig deviceConfig = null;
                //nvrCmd.GetDeviceConfig(ref deviceConfigXml, deviceId);

                //XmlDocument xdoc = new XmlDocument();
                try
                {
                    //xdoc.LoadXml(deviceConfigXml);
                    //XmlNodeReader reader = new XmlNodeReader(xdoc.DocumentElement);
                    //XmlSerializer ser = new XmlSerializer(typeof(DeviceConfig));
                    //deviceConfig = (DeviceConfig)ser.Deserialize(reader);
                    //var resolutionList = deviceConfig.Device.VideoQuality.Quality.Resolution1.Split('x');
                    //var widthStr = resolutionList[0].Replace("N", "");
                    //var heightStr = resolutionList[1];
                    //TODO get framerate instead of hardcode
                }
                catch (Exception ex)
                {
                    _logger.Error(ex.ToString());
                }
                //TODO open wmfplayer by session (device + stream or device + stream + IP&Port + playback time)
                WmfPlayer wmfPlayer = new WmfPlayer(new IntPtr(Int32.Parse(deviceId)));
                wmfPlayer.m_SelectID = deviceId;
                if (_nvrPlayerList.TryAdd(deviceId, wmfPlayer))
                {
                    wmfPlayer.ReceivedYUVFrame += video_source_ReceivedYUVFrame;
                    await wmfPlayer.OpenVideoAsync(_nvrIp, uint.Parse(_nvrPort), authInfo.Username, authInfo.Password, startTime, 1, deviceId, int.Parse(streamId));
                }
            }
        }
        else if (message is Rtsp.Messages.RtspRequestDescribe) // Handle DESCRIBE message
        {
            String requested_url = (message as Rtsp.Messages.RtspRequestDescribe).RtspUri.ToString();
            Console.WriteLine("Request for " + requested_url);

            // TODO. Check the requsted_url is valid. In this example we accept any RTSP URL
            // Make the Base64 SPS and PPS
            raw_sps = h264_encoder.GetRawSPS(); // no 0x00 0x00 0x00 0x01 or 32 bit size header
            raw_pps = h264_encoder.GetRawPPS(); // no 0x00 0x00 0x00 0x01 or 32 bit size header
            String sps_str = Convert.ToBase64String(raw_sps);
            String pps_str = Convert.ToBase64String(raw_pps);

            StringBuilder sdp = new StringBuilder();

            // Generate the SDP
            // The sprop-parameter-sets provide the SPS and PPS for H264 video
            // The packetization-mode defines the H264 over RTP payloads used but is Optional
            sdp.Append("v=0\n");
            sdp.Append($"o={authInfo.Username} 0 0 IN IP4 0.0.0.0\n");
            sdp.Append("s=ACTi NVR\n");
            sdp.Append("m=video 0 RTP/AVP 96\n");
            sdp.Append("c=IN IP4 0.0.0.0\n");
            sdp.Append("a=control:*\n");
            sdp.Append("a=rtpmap:96 H264/90000\n");
            sdp.Append("a=fmtp:96 packetization-mode=1;profile-level-id=4D6028; sprop-parameter-sets=" + sps_str + "," + pps_str + ";\n");

            byte[] sdp_bytes = Encoding.ASCII.GetBytes(sdp.ToString());

            // Create the reponse to DESCRIBE
            // This must include the Session Description Protocol (SDP)
            Rtsp.Messages.RtspResponse describe_response = (e.Message as Rtsp.Messages.RtspRequestDescribe).CreateResponse();

            describe_response.AddHeader("Content-Base: " + requested_url);
            describe_response.AddHeader("Content-Type: application/sdp");
            describe_response.Data = sdp_bytes;
            describe_response.AdjustContentLength();
            listener.SendMessage(describe_response);
        }
        else if (message is Rtsp.Messages.RtspRequestSetup)// Handle SETUP message
        {
            //
            var setupMessage = message as Rtsp.Messages.RtspRequestSetup;

            // Check the RTSP transport
            // If it is UDP or Multicast, create the sockets
            // If it is RTP over RTSP we send data via the RTSP Listener

            // FIXME client may send more than one possible transport.
            // very rare
            Rtsp.Messages.RtspTransport transport = setupMessage.GetTransports()[0];


            // Construct the Transport: reply from the Server to the client
            Rtsp.Messages.RtspTransport transport_reply = new Rtsp.Messages.RtspTransport();
            transport_reply.SSrc = global_ssrc.ToString("X8"); // Convert to Hex, padded to 8 characters

            if (transport.LowerTransport == Rtsp.Messages.RtspTransport.LowerTransportType.TCP)
            {
                // RTP over RTSP mode}
                transport_reply.LowerTransport = Rtsp.Messages.RtspTransport.LowerTransportType.TCP;
                transport_reply.Interleaved    = new Rtsp.Messages.PortCouple(transport.Interleaved.First, transport.Interleaved.Second);
            }

            Rtsp.UDPSocket udp_pair = null;
            if (transport.LowerTransport == Rtsp.Messages.RtspTransport.LowerTransportType.UDP &&
                transport.IsMulticast == false)
            {
                Boolean udp_supported = true;
                if (udp_supported)
                {
                    // RTP over UDP mode
                    // Create a pair of UDP sockets - One is for the Video, one is for the RTCP
                    udp_pair = new Rtsp.UDPSocket(50000, 51000); // give a range of 500 pairs (1000 addresses) to try incase some address are in use
                    udp_pair.DataReceived += (object local_sender, RtspChunkEventArgs local_e) => {
                        // RTCP data received
                        Console.WriteLine("RTCP data received " + local_sender.ToString() + " " + local_e.ToString());
                    };
                    udp_pair.Start(); // start listening for data on the UDP ports

                    // Pass the Port of the two sockets back in the reply
                    transport_reply.LowerTransport = Rtsp.Messages.RtspTransport.LowerTransportType.UDP;
                    transport_reply.IsMulticast    = false;
                    transport_reply.ClientPort     = new Rtsp.Messages.PortCouple(udp_pair.data_port, udp_pair.control_port);
                }
                else
                {
                    transport_reply = null;
                }
            }

            if (transport.LowerTransport == Rtsp.Messages.RtspTransport.LowerTransportType.UDP &&
                transport.IsMulticast == true)
            {
                // RTP over Multicast UDP mode}
                // Create a pair of UDP sockets in Multicast Mode
                // Pass the Ports of the two sockets back in the reply
                transport_reply.LowerTransport = Rtsp.Messages.RtspTransport.LowerTransportType.UDP;
                transport_reply.IsMulticast    = true;
                transport_reply.Port           = new Rtsp.Messages.PortCouple(7000, 7001); // FIX

                // for now until implemented
                transport_reply = null;
            }


            if (transport_reply != null)
            {
                // Update the session with transport information
                String copy_of_session_id = "";
                lock (rtsp_list)
                {
                    foreach (RTSPConnection connection in rtsp_list)
                    {
                        if (connection.listener.RemoteAdress.Equals(listener.RemoteAdress))
                        {
                            // ToDo - Check the Track ID to determine if this is a SETUP for the Video Stream
                            // or a SETUP for an Audio Stream.
                            // In the SDP the H264 video track is TrackID 0


                            // found the connection
                            // Add the transports to the connection
                            connection.video_client_transport = transport;
                            connection.video_transport_reply  = transport_reply;

                            // If we are sending in UDP mode, add the UDP Socket pair and the Client Hostname
                            connection.video_udp_pair = udp_pair;


                            connection.video_session_id = session_handle.ToString();
                            session_handle++;


                            // Copy the Session ID
                            copy_of_session_id = connection.video_session_id;
                            break;
                        }
                    }
                }

                Rtsp.Messages.RtspResponse setup_response = setupMessage.CreateResponse();
                setup_response.Headers[Rtsp.Messages.RtspHeaderNames.Transport] = transport_reply.ToString();
                setup_response.Session = copy_of_session_id;
                listener.SendMessage(setup_response);
            }
            else
            {
                Rtsp.Messages.RtspResponse setup_response = setupMessage.CreateResponse();
                // unsuported transport
                setup_response.ReturnCode = 461;
                listener.SendMessage(setup_response);
            }
        }
        else if (message is Rtsp.Messages.RtspRequestPlay)// Handle PLAY message (Sent with a Session ID)
        {
            lock (rtsp_list)
            {
                // Search for the Session in the Sessions List. Change the state to "PLAY"
                bool session_found = false;
                foreach (RTSPConnection connection in rtsp_list)
                {
                    if (message.Session == connection.video_session_id) /* OR AUDIO_SESSION_ID */
                    {
                        // found the session
                        session_found   = true;
                        connection.play = true;                                                                                                   // ACTUALLY YOU COULD PAUSE JUST THE VIDEO (or JUST THE AUDIO)

                        string range    = "npt=0-";                                                                                               // Playing the 'video' from 0 seconds until the end
                        string rtp_info = "url=" + ((Rtsp.Messages.RtspRequestPlay)message).RtspUri + ";seq=" + connection.video_sequence_number; // TODO Add rtptime  +";rtptime="+session.rtp_initial_timestamp;

                        // Send the reply
                        Rtsp.Messages.RtspResponse play_response = (e.Message as Rtsp.Messages.RtspRequestPlay).CreateResponse();
                        play_response.AddHeader("Range: " + range);
                        play_response.AddHeader("RTP-Info: " + rtp_info);
                        listener.SendMessage(play_response);

                        break;
                    }
                }

                if (session_found == false)
                {
                    // Session ID was not found in the list of Sessions. Send a 454 error
                    Rtsp.Messages.RtspResponse play_failed_response = (e.Message as Rtsp.Messages.RtspRequestPlay).CreateResponse();
                    play_failed_response.ReturnCode = 454; // Session Not Found
                    listener.SendMessage(play_failed_response);
                }
            }
        }
        else if (message is Rtsp.Messages.RtspRequestPause) // Handle PAUSE message (Sent with a Session ID)
        {
            lock (rtsp_list)
            {
                // Search for the Session in the Sessions List. Change the state of "PLAY"
                foreach (RTSPConnection connection in rtsp_list)
                {
                    if (message.Session == connection.video_session_id /* OR AUDIO SESSION ID */)
                    {
                        // found the session
                        connection.play = false; // COULD HAVE PLAY/PAUSE FOR VIDEO AND AUDIO
                        break;
                    }
                }
            }

            // ToDo - only send back the OK response if the Session in the RTSP message was found
            Rtsp.Messages.RtspResponse pause_response = (e.Message as Rtsp.Messages.RtspRequestPause).CreateResponse();
            listener.SendMessage(pause_response);
        }


        // Handle GET_PARAMETER message, often used as a Keep Alive
        if (message is Rtsp.Messages.RtspRequestGetParameter)
        {
            // Create the reponse to GET_PARAMETER
            Rtsp.Messages.RtspResponse getparameter_response = (e.Message as Rtsp.Messages.RtspRequestGetParameter).CreateResponse();
            listener.SendMessage(getparameter_response);
        }


        // Handle TEARDOWN (sent with a Session ID)
        if (message is Rtsp.Messages.RtspRequestTeardown)
        {
            lock (rtsp_list)
            {
                // Search for the Session in the Sessions List.
                foreach (RTSPConnection connection in rtsp_list.ToArray()) // Convert to ToArray so we can delete from the rtp_list
                {
                    if (message.Session == connection.video_session_id)    // SHOULD HAVE AN AUDIO TEARDOWN AS WELL
                    {
                        // If this is UDP, close the transport
                        // For TCP there is no transport to close (as RTP packets were interleaved into the RTSP connection)
                        if (connection.video_udp_pair != null)
                        {
                            connection.video_udp_pair.Stop();
                            connection.video_udp_pair = null;
                        }

                        rtsp_list.Remove(connection);

                        // Close the RTSP socket
                        listener.Dispose();
                    }
                }
            }
        }
    }
Exemple #10
0
        /// <summary>
        /// Handles the request play.
        /// Do not forward message if already playing
        /// </summary>
        /// <param name="destination">The destination.</param>
        /// <param name="requestPlay">The request play.</param>
        /// <returns>The message to transmit</returns>
        private RtspMessage HandleRequestPlay(ref RtspListener destination, RtspRequestPlay requestPlay)
        {
            Contract.Requires(requestPlay != null);
            Contract.Requires(destination != null);
            Contract.Ensures(Contract.Result<RtspMessage>() != null);
            Contract.Ensures(Contract.ValueAtReturn(out destination) != null);

            string sessionKey = RtspSession.GetSessionName(requestPlay.RtspUri, requestPlay.Session);
            if (_activesSession.ContainsKey(sessionKey))
            {

                RtspSession session = _activesSession[sessionKey];

                // si on est dèjà en play on n'envoie pas la commande a la source.
                if (session.State == RtspSession.SessionState.Playing)
                {
                    session.Start(requestPlay.SourcePort.RemoteAdress);
                    RtspResponse returnValue = requestPlay.CreateResponse();
                    destination = requestPlay.SourcePort;
                    return returnValue;
                }

                // ajoute un client
                session.Start(requestPlay.SourcePort.RemoteAdress);
            }
            return requestPlay;
        }
Exemple #11
0
    // Process each RTSP message that is received
    private void RTSP_Message_Received(object sender, RtspChunkEventArgs e)
    {
        // Cast the 'sender' and 'e' into the RTSP Listener (the Socket) and the RTSP Message
        Rtsp.RtspListener         listener = sender as Rtsp.RtspListener;
        Rtsp.Messages.RtspMessage message  = e.Message as Rtsp.Messages.RtspMessage;

        Console.WriteLine("RTSP message received " + message);

        // Handle OPTIONS message
        if (message is Rtsp.Messages.RtspRequestOptions)
        {
            // Create the reponse to OPTIONS
            Rtsp.Messages.RtspResponse options_response = (e.Message as Rtsp.Messages.RtspRequestOptions).CreateResponse();
            listener.SendMessage(options_response);
        }

        // Handle DESCRIBE message
        if (message is Rtsp.Messages.RtspRequestDescribe)
        {
            String requested_url = (message as Rtsp.Messages.RtspRequestDescribe).RtspUri.ToString();
            Console.WriteLine("Request for " + requested_url);

            // TODO. Check the requsted_url is valid. In this example we accept any RTSP URL

            // Make the Base64 SPS and PPS
            byte[] raw_sps = h264_encoder.GetRawSPS(); // no 0x00 0x00 0x00 0x01 or 32 bit size header
            byte[] raw_pps = h264_encoder.GetRawPPS(); // no 0x00 0x00 0x00 0x01 or 32 bit size header
            String sps_str = Convert.ToBase64String(raw_sps);
            String pps_str = Convert.ToBase64String(raw_pps);

            StringBuilder sdp = new StringBuilder();

            // Generate the SDP
            // The sprop-parameter-sets provide the SPS and PPS for H264 video
            // The packetization-mode defines the H264 over RTP payloads used but is Optional
            sdp.Append("v=0\n");
            sdp.Append("o=user 123 0 IN IP4 0.0.0.0\n");
            sdp.Append("s=SharpRTSP Test Camera\n");
            sdp.Append("m=video 0 RTP/AVP 96\n");
            sdp.Append("c=IN IP4 0.0.0.0\n");
            sdp.Append("a=control:trackID=0\n");
            sdp.Append("a=rtpmap:96 H264/90000\n");
            sdp.Append("a=fmtp:96 profile-level-id=42A01E; sprop-parameter-sets=" + sps_str + "," + pps_str + ";\n");

            byte[] sdp_bytes = Encoding.ASCII.GetBytes(sdp.ToString());

            // Create the reponse to DESCRIBE
            // This must include the Session Description Protocol (SDP)
            Rtsp.Messages.RtspResponse describe_response = (e.Message as Rtsp.Messages.RtspRequestDescribe).CreateResponse();

            describe_response.AddHeader("Content-Base: " + requested_url);
            describe_response.AddHeader("Content-Type: application/sdp");
            describe_response.Data = sdp_bytes;
            describe_response.AdjustContentLength();
            listener.SendMessage(describe_response);
        }

        // Handle SETUP message
        if (message is Rtsp.Messages.RtspRequestSetup)
        {
            //
            var setupMessage = message as Rtsp.Messages.RtspRequestSetup;

            // Check the RTSP transport
            // If it is UDP or Multicast, create the sockets
            // If it is RTP over RTSP we send data via the RTSP Listener

            // FIXME client may send more than one possible transport.
            // very rare
            Rtsp.Messages.RtspTransport transport = setupMessage.GetTransports()[0];


            // Construct the Transport: reply from the Server to the client
            Rtsp.Messages.RtspTransport transport_reply = new Rtsp.Messages.RtspTransport();

            if (transport.LowerTransport == Rtsp.Messages.RtspTransport.LowerTransportType.TCP)
            {
                // RTP over RTSP mode}
                transport_reply.LowerTransport = Rtsp.Messages.RtspTransport.LowerTransportType.TCP;
                transport_reply.Interleaved    = new Rtsp.Messages.PortCouple(transport.Interleaved.First, transport.Interleaved.Second);
            }

            if (transport.LowerTransport == Rtsp.Messages.RtspTransport.LowerTransportType.UDP &&
                transport.IsMulticast == false)
            {
                // RTP over UDP mode}
                // Create a pair of UDP sockets
                // Pass the Port of the two sockets back in the reply
                transport_reply.LowerTransport = Rtsp.Messages.RtspTransport.LowerTransportType.UDP;
                transport_reply.IsMulticast    = false;
                transport_reply.ClientPort     = transport.ClientPort; // FIX
                                                                       // for now until implemented
                transport_reply = null;
            }

            if (transport.LowerTransport == Rtsp.Messages.RtspTransport.LowerTransportType.UDP &&
                transport.IsMulticast == true)
            {
                // RTP over Multicast UDP mode}
                // Create a pair of UDP sockets in Multicast Mode
                // Pass the Ports of the two sockets back in the reply
                transport_reply.LowerTransport = Rtsp.Messages.RtspTransport.LowerTransportType.UDP;
                transport_reply.IsMulticast    = true;
                transport_reply.Port           = new Rtsp.Messages.PortCouple(7000, 7001); // FIX

                // for now until implemented
                transport_reply = null;
            }


            if (transport_reply != null)
            {
                RTPSession new_session = new RTPSession();
                new_session.listener        = listener;
                new_session.sequence_number = (UInt16)rnd.Next(65535); // start with a random 16 bit sequence number
                new_session.ssrc            = 1;

                // Add the transports to the Session
                new_session.client_transport = transport;
                new_session.transport_reply  = transport_reply;

                lock (rtp_list)
                {
                    // Create a 'Session' and add it to the Session List
                    // ToDo - Check the Track ID. In the SDP the H264 video track is TrackID 0
                    // Place Lock() here so the Session Count and the addition to the list is locked
                    new_session.session_id = session_count.ToString();

                    // Add the new session to the Sessions List
                    rtp_list.Add(new_session);
                    session_count++;
                }


                Rtsp.Messages.RtspResponse setup_response = setupMessage.CreateResponse();
                setup_response.Headers[Rtsp.Messages.RtspHeaderNames.Transport] = transport_reply.ToString();
                setup_response.Session = new_session.session_id;
                listener.SendMessage(setup_response);
            }
            else
            {
                Rtsp.Messages.RtspResponse setup_response = setupMessage.CreateResponse();
                // unsuported transport
                setup_response.ReturnCode = 461;
                listener.SendMessage(setup_response);
            }
        }

        // Handle PLAY message
        if (message is Rtsp.Messages.RtspRequestPlay)
        {
            lock (rtp_list)
            {
                // Search for the Session in the Sessions List. Change the state of "PLAY"
                foreach (RTPSession session in rtp_list)
                {
                    if (session.session_id.Equals(message.Session))
                    {
                        // found the session
                        session.play = true;
                        break;
                    }
                }
            }

            // ToDo - only send back the OK response if the Session in the RTSP message was found
            Rtsp.Messages.RtspResponse play_response = (e.Message as Rtsp.Messages.RtspRequestPlay).CreateResponse();
            listener.SendMessage(play_response);
        }

        // Handle PLAUSE message
        if (message is Rtsp.Messages.RtspRequestPause)
        {
            lock (rtp_list)
            {
                // Search for the Session in the Sessions List. Change the state of "PLAY"
                foreach (RTPSession session in rtp_list)
                {
                    if (session.session_id.Equals(message.Session))
                    {
                        // found the session
                        session.play = false;
                        break;
                    }
                }
            }

            // ToDo - only send back the OK response if the Session in the RTSP message was found
            Rtsp.Messages.RtspResponse pause_response = (e.Message as Rtsp.Messages.RtspRequestPause).CreateResponse();
            listener.SendMessage(pause_response);
        }


        // Handle GET_PARAMETER message, often used as a Keep Alive
        if (message is Rtsp.Messages.RtspRequestGetParameter)
        {
            // Create the reponse to GET_PARAMETER
            Rtsp.Messages.RtspResponse getparameter_response = (e.Message as Rtsp.Messages.RtspRequestGetParameter).CreateResponse();
            listener.SendMessage(getparameter_response);
        }


        // Handle TEARDOWN
        if (message is Rtsp.Messages.RtspRequestTeardown)
        {
            lock (rtp_list)
            {
                // Search for the Session in the Sessions List.
                foreach (RTPSession session in rtp_list.ToArray()) // Convert to ToArray so we can delete from the rtp_list
                {
                    if (session.session_id.Equals(message.Session))
                    {
                        // TODO - Close UDP or Multicast transport
                        // For TCP there is no transport to close
                        rtp_list.Remove(session);
                        // Close the RTSP socket
                        listener.Dispose();
                    }
                }
            }
        }
    }
Exemple #12
0
        public volatile RtspStatus CurrentStatus; // Connecting, Connected etc

        // Constructor
        public RTSPClient(String url, RTP_TRANSPORT rtp_transport)
        {
            Rtsp.RtspUtils.RegisterUri();

            Console.WriteLine("Connecting to " + url);
            this.url = url;

            // Use URI to extract host, port, username and password
            Uri uri = new Uri(this.url);

            if (uri.UserInfo.Length > 0)
            {
                try {
                    username = uri.UserInfo.Split(new char[] { ':' })[0];
                    password = uri.UserInfo.Split(new char[] { ':' })[1];
                    this.url = uri.GetComponents((UriComponents.AbsoluteUri & ~UriComponents.UserInfo),
                                                 UriFormat.UriEscaped);
                    uri = new Uri(this.url);
                } catch {
                    username = null;
                    password = null;
                }
            }

            Rtsp_Client_StatusChanged(this, new RtspStatusEventArgs(RtspStatus.Connecting));

            // Connect to a RTSP Server. The RTSP session is a TCP connection
            try
            {
                rtsp_socket = new Rtsp.RtspTcpTransport(uri.Host, uri.Port);
            }
            catch
            {
                Console.WriteLine("Error - did not connect");
                Rtsp_Client_StatusChanged(this, new RtspStatusEventArgs(RtspStatus.ConnectFailed));
                return;
            }

            if (rtsp_socket.Connected == false)
            {
                Console.WriteLine("Error - did not connect");
                Rtsp_Client_StatusChanged(this, new RtspStatusEventArgs(RtspStatus.ConnectFailed));
                return;
            }

            Rtsp_Client_StatusChanged(this, new RtspStatusEventArgs(RtspStatus.Connected));


            String now = DateTime.Now.ToString("yyyyMMdd_HHmmss");

            if (write_log_files == true)
            {
                String filename = "rtsp_capture_" + now + ".264";
                fs = new FileStream(filename, FileMode.Create);
            }
            if (fs2 == null)
            {
                String filename2 = "rtsp_capture_" + now + ".raw";
                fs2 = new StreamWriter(filename2);
            }


            // Connect a RTSP Listener to the RTSP Socket (or other Stream) to send RTSP messages and listen for RTSP replies
            rtsp_client = new Rtsp.RtspListener(rtsp_socket);

            rtsp_client.MessageReceived += Rtsp_MessageReceived;
            rtsp_client.DataReceived    += Rtp_DataReceived;
            rtsp_client.StatusChanged   += Rtsp_Client_StatusChanged;

            rtsp_client.Start(); // start listening for messages from the server (messages fire the MessageReceived event)


            // Check the RTP Transport
            // If the RTP transport is TCP then we interleave the RTP packets in the RTSP stream
            // If the RTP transport is UDP, we initialise two UDP sockets (one for video, one for RTCP status messages)
            // If the RTP transport is MULTICAST, we have to wait for the SETUP message to get the Multicast Address from the RTSP server
            this.rtp_transport = rtp_transport;
            if (rtp_transport == RTP_TRANSPORT.UDP)
            {
                udp_pair = new UDPSocket(50000, 50020); // give a range of 10 pairs (20 addresses) to try incase some address are in use
                udp_pair.DataReceived += Rtp_DataReceived;
                udp_pair.Start();                       // start listening for data on the UDP ports
            }
            if (rtp_transport == RTP_TRANSPORT.TCP)
            {
                // Nothing to do. Data will arrive in the RTSP Listener
            }
            if (rtp_transport == RTP_TRANSPORT.MULTICAST)
            {
                // Nothing to do. Will open Multicast UDP sockets after the SETUP command
            }


            // Send OPTIONS
            // In the Received Message handler we will send DESCRIBE, SETUP and PLAY
            Rtsp.Messages.RtspRequest options_message = new Rtsp.Messages.RtspRequestOptions();
            options_message.RtspUri = new Uri(this.url);
            rtsp_client.SendMessage(options_message);
        }
Exemple #13
0
 /// <summary>
 /// Accepts the connection.
 /// </summary>
 private void AcceptConnection()
 {
     try
         {
             while (!_Stopping.WaitOne(0))
             {
                 TcpClient oneClient = _RTSPServerListener.AcceptTcpClient();
                 var rtsp_socket = new RtspTcpTransport(oneClient);
                 RtspListener newListener = new RtspListener(rtsp_socket);
                 newListener.MessageReceived += RTSP_Message_Received;
                 //RTSPDispatcher.Instance.AddListener(newListener);
                 newListener.Start();
             }
         }
         catch (SocketException error)
         {
            // _logger.Warn("Got an error listening, I have to handle the stopping which also throw an error", error);
         }
         catch (Exception error)
         {
            // _logger.Error("Got an error listening...", error);
             throw;
         }
 }
Exemple #14
0
    // Process each RTSP message that is received
    private void RTSP_Message_Received(object sender, RtspChunkEventArgs e)
    {
        // Cast the 'sender' and 'e' into the RTSP Listener (the Socket) and the RTSP Message
        Rtsp.RtspListener         listener = sender as Rtsp.RtspListener;
        Rtsp.Messages.RtspMessage message  = e.Message as Rtsp.Messages.RtspMessage;

        Console.WriteLine("RTSP message received " + message);


        // Check if the RTSP Message has valid authentication (validating against username,password,realm and nonce)
        if (auth != null)
        {
            bool authorized = false;
            if (message.Headers.ContainsKey("Authorization") == true)
            {
                // Check the message has the correct Authorization
                authorized = auth.IsValid(message);
            }
            if ((message.Headers.ContainsKey("Authorization") == false) ||
                authorized == false)
            {
                // Send a 401 Authentication Required reply
                Rtsp.Messages.RtspResponse authorization_response = (e.Message as Rtsp.Messages.RtspRequest).CreateResponse();
                authorization_response.AddHeader("WWW-Authenticate: " + auth.GetHeader());
                authorization_response.ReturnCode = 401;
                listener.SendMessage(authorization_response);
                return;
            }
        }

        // Handle OPTIONS message
        if (message is Rtsp.Messages.RtspRequestOptions)
        {
            // Create the reponse to OPTIONS
            Rtsp.Messages.RtspResponse options_response = (e.Message as Rtsp.Messages.RtspRequestOptions).CreateResponse();
            listener.SendMessage(options_response);
        }

        // Handle DESCRIBE message
        if (message is Rtsp.Messages.RtspRequestDescribe)
        {
            String requested_url = (message as Rtsp.Messages.RtspRequestDescribe).RtspUri.ToString();
            Console.WriteLine("Request for " + requested_url);

            // TODO. Check the requsted_url is valid. In this example we accept any RTSP URL

            //// Make the Base64 SPS and PPS
            //byte[] raw_sps = h264_encoder.GetRawSPS(); // no 0x00 0x00 0x00 0x01 or 32 bit size header
            //byte[] raw_pps = h264_encoder.GetRawPPS(); // no 0x00 0x00 0x00 0x01 or 32 bit size header
            //String sps_str = Convert.ToBase64String(raw_sps);
            //String pps_str = Convert.ToBase64String(raw_pps);

            StringBuilder sdp = new StringBuilder();

            // Generate the SDP
            // The sprop-parameter-sets provide the SPS and PPS for H264 video
            // The packetization-mode defines the H264 over RTP payloads used but is Optional
            sdp.Append("v=0\n");
            sdp.Append("o=user 123 0 IN IP4 0.0.0.0\n");
            sdp.Append("s=WPFRtspServer\n");
            sdp.Append("c=IN IP4 0.0.0.0\n");

            if (mStreams != null)
            {
                foreach (var item in mStreams)
                {
                    switch (item.Item1)
                    {
                    case StreamType.Video:
                    {
                        sdp.Append(string.Format("m=video 0 RTP/AVP {0}\n", (uint)item.Item1));
                        sdp.Append(string.Format("a=rtpmap:{0} {1}/90000\n", (uint)item.Item1, item.Item3));
                        sdp.Append(string.Format("a=fmtp:{0}\n", (uint)item.Item1));
                        sdp.Append(string.Format("a=control:trackID={0}\n", item.Item2));
                        //sdp.Append("a=fmtp:96 profile-level-id=42A01E; sprop-parameter-sets=" + sps_str + "," + pps_str + ";\n");
                    }
                    break;

                    case StreamType.Audio:
                    {
                        sdp.Append(string.Format("m=audio 0 RTP/AVP {0}\n", (uint)item.Item1));
                        sdp.Append(string.Format("a=rtpmap:{0} mpeg4-generic/90000/2\n", (uint)item.Item1));
                        sdp.Append(string.Format("a=fmtp:{0}\n", (uint)item.Item1));
                        sdp.Append(string.Format("a=control:trackID={0}\n", item.Item2));
                        //sdp.Append("a=fmtp:96 profile-level-id=42A01E; sprop-parameter-sets=" + sps_str + "," + pps_str + ";\n");
                    }
                    break;

                    case StreamType.None:
                    default:
                        break;
                    }
                }
            }

            byte[] sdp_bytes = Encoding.ASCII.GetBytes(sdp.ToString());

            // Create the reponse to DESCRIBE
            // This must include the Session Description Protocol (SDP)
            Rtsp.Messages.RtspResponse describe_response = (e.Message as Rtsp.Messages.RtspRequestDescribe).CreateResponse();

            describe_response.AddHeader("Content-Base: " + requested_url);
            describe_response.AddHeader("Content-Type: application/sdp");
            describe_response.Data = sdp_bytes;
            describe_response.AdjustContentLength();
            listener.SendMessage(describe_response);
        }

        // Handle SETUP message
        if (message is Rtsp.Messages.RtspRequestSetup)
        {
            //
            var setupMessage = message as Rtsp.Messages.RtspRequestSetup;

            // Check the RTSP transport
            // If it is UDP or Multicast, create the sockets
            // If it is RTP over RTSP we send data via the RTSP Listener

            // FIXME client may send more than one possible transport.
            // very rare
            Rtsp.Messages.RtspTransport transport = setupMessage.GetTransports()[0];


            // Construct the Transport: reply from the Server to the client
            Rtsp.Messages.RtspTransport transport_reply = new Rtsp.Messages.RtspTransport();
            transport_reply.SSrc = global_ssrc.ToString("X8"); // Convert to Hex, padded to 8 characters

            if (transport.LowerTransport == Rtsp.Messages.RtspTransport.LowerTransportType.TCP)
            {
                // RTP over RTSP mode}
                transport_reply.LowerTransport = Rtsp.Messages.RtspTransport.LowerTransportType.TCP;
                transport_reply.Interleaved    = new Rtsp.Messages.PortCouple(transport.Interleaved.First, transport.Interleaved.Second);
            }

            if (transport.LowerTransport == Rtsp.Messages.RtspTransport.LowerTransportType.UDP &&
                transport.IsMulticast == false)
            {
                // RTP over UDP mode}
                // Create a pair of UDP sockets
                // Pass the Port of the two sockets back in the reply
                transport_reply.LowerTransport = Rtsp.Messages.RtspTransport.LowerTransportType.UDP;
                transport_reply.IsMulticast    = false;
                transport_reply.ClientPort     = transport.ClientPort; // FIX
                                                                       // for now until implemented
                transport_reply = null;
            }

            if (transport.LowerTransport == Rtsp.Messages.RtspTransport.LowerTransportType.UDP &&
                transport.IsMulticast == true)
            {
                // RTP over Multicast UDP mode}
                // Create a pair of UDP sockets in Multicast Mode
                // Pass the Ports of the two sockets back in the reply
                transport_reply.LowerTransport = Rtsp.Messages.RtspTransport.LowerTransportType.UDP;
                transport_reply.IsMulticast    = true;
                transport_reply.Port           = new Rtsp.Messages.PortCouple(7000, 7001); // FIX

                // for now until implemented
                transport_reply = null;
            }


            if (transport_reply != null)
            {
                RTPSession new_session = new RTPSession();
                new_session.listener        = listener;
                new_session.sequence_number = (UInt16)rnd.Next(65535); // start with a random 16 bit sequence number
                new_session.ssrc            = global_ssrc;

                // Add the transports to the Session
                new_session.client_transport = transport;
                new_session.transport_reply  = transport_reply;

                lock (rtp_list)
                {
                    if (setupMessage.RtspUri.Segments != null && setupMessage.RtspUri.Segments.Length == 2)
                    {
                        var split = setupMessage.RtspUri.Segments[1].Split(new char[] { '=' });

                        if (split != null && split.Length == 2)
                        {
                            int.TryParse(split[1], out new_session.trackID);
                        }
                    }



                    //setupMessage.SourcePort.RemoteAdress

                    // Create a 'Session' and add it to the Session List
                    // ToDo - Check the Track ID. In the SDP the H264 video track is TrackID 0
                    // Place Lock() here so the Session Count and the addition to the list is locked
                    new_session.session_id = session_count.ToString();

                    // Add the new session to the Sessions List
                    rtp_list.Add(new_session);
                    session_count++;
                }


                Rtsp.Messages.RtspResponse setup_response = setupMessage.CreateResponse();
                setup_response.Headers[Rtsp.Messages.RtspHeaderNames.Transport] = transport_reply.ToString();
                setup_response.Session = new_session.session_id;
                listener.SendMessage(setup_response);
            }
            else
            {
                Rtsp.Messages.RtspResponse setup_response = setupMessage.CreateResponse();
                // unsuported transport
                setup_response.ReturnCode = 461;
                listener.SendMessage(setup_response);
            }
        }

        // Handle PLAY message
        if (message is Rtsp.Messages.RtspRequestPlay)
        {
            lock (rtp_list)
            {
                // Search for the Session in the Sessions List. Change the state of "PLAY"
                bool session_found = false;
                foreach (RTPSession session in rtp_list)
                {
                    //if (session.session_id.Equals(message.Session))
                    {
                        // found the session
                        session_found = true;
                        session.play  = true;

                        string range    = "npt=0-";                                                                                      // Playing the 'video' from 0 seconds until the end
                        string rtp_info = "url=" + ((Rtsp.Messages.RtspRequestPlay)message).RtspUri + ";seq=" + session.sequence_number; // TODO Add rtptime  +";rtptime="+session.rtp_initial_timestamp;

                        // Send the reply
                        Rtsp.Messages.RtspResponse play_response = (e.Message as Rtsp.Messages.RtspRequestPlay).CreateResponse();
                        play_response.AddHeader("Range: " + range);
                        play_response.AddHeader("RTP-Info: " + rtp_info);
                        listener.SendMessage(play_response);
                    }
                }

                if (session_found == false)
                {
                    // Session ID was not found in the list of Sessions. Send a 454 error
                    Rtsp.Messages.RtspResponse play_failed_response = (e.Message as Rtsp.Messages.RtspRequestPlay).CreateResponse();
                    play_failed_response.ReturnCode = 454; // Session Not Found
                    listener.SendMessage(play_failed_response);
                }
            }
        }

        // Handle PLAUSE message
        if (message is Rtsp.Messages.RtspRequestPause)
        {
            lock (rtp_list)
            {
                // Search for the Session in the Sessions List. Change the state of "PLAY"
                foreach (RTPSession session in rtp_list)
                {
                    if (session.session_id.Equals(message.Session))
                    {
                        // found the session
                        session.play = false;
                        break;
                    }
                }
            }

            // ToDo - only send back the OK response if the Session in the RTSP message was found
            Rtsp.Messages.RtspResponse pause_response = (e.Message as Rtsp.Messages.RtspRequestPause).CreateResponse();
            listener.SendMessage(pause_response);
        }


        // Handle GET_PARAMETER message, often used as a Keep Alive
        if (message is Rtsp.Messages.RtspRequestGetParameter)
        {
            // Create the reponse to GET_PARAMETER
            Rtsp.Messages.RtspResponse getparameter_response = (e.Message as Rtsp.Messages.RtspRequestGetParameter).CreateResponse();
            listener.SendMessage(getparameter_response);
        }


        // Handle TEARDOWN
        if (message is Rtsp.Messages.RtspRequestTeardown)
        {
            lock (rtp_list)
            {
                // Search for the Session in the Sessions List.
                foreach (RTPSession session in rtp_list.ToArray()) // Convert to ToArray so we can delete from the rtp_list
                {
                    if (session.session_id.Equals(message.Session))
                    {
                        // TODO - Close UDP or Multicast transport
                        // For TCP there is no transport to close
                        rtp_list.Remove(session);
                        // Close the RTSP socket
                        listener.Dispose();
                    }
                }
            }
        }
    }