private void ContinueAfterPlay(RTSPClient rtspClient, int resultCode, string resultString) { do { var scs = ((OurRtspClient)rtspClient).scs; // alias if (resultCode != 0) { Console.Error.WriteLine (rtspClient + "Failed to start playing session: " + resultString); break; } if (scs.duration > 0) { const uint delaySlop = 2; // number of seconds extra to delay, after the stream's expected duration. (This is optional.) scs.duration += delaySlop; var uSecsToDelay = (long)(scs.duration * 1000000); scs.streamTimerTask = env.TaskScheduler.ScheduleDelayedTask (uSecsToDelay, streamTimerHandler, rtspClient.Native.Native); } Console.Error.Write (rtspClient + "Started playing session"); if (scs.duration > 0) { Console.Error.Write (" (for up to " + scs.duration + " seconds)"); } Console.Error.WriteLine ("..."); return; } while (false); // An unrecoverable error occurred with this stream. ShutdownStream (rtspClient); }
private void ContinueAfterPlay(RTSPClient rtspClient, int resultCode, string resultString) { do { var scs = ((OurRtspClient)rtspClient).scs; // alias if (resultCode != 0) { Console.Error.WriteLine(rtspClient + "Failed to start playing session: " + resultString); break; } if (scs.duration > 0) { const uint delaySlop = 2; // number of seconds extra to delay, after the stream's expected duration. (This is optional.) scs.duration += delaySlop; var uSecsToDelay = (long)(scs.duration * 1000000); scs.streamTimerTask = env.TaskScheduler.ScheduleDelayedTask(uSecsToDelay, streamTimerHandler, rtspClient.Native.Native); } Console.Error.Write(rtspClient + "Started playing session"); if (scs.duration > 0) { Console.Error.Write(" (for up to " + scs.duration + " seconds)"); } Console.Error.WriteLine("..."); return; } while (false); // An unrecoverable error occurred with this stream. ShutdownStream(rtspClient); }
private void ContinueAfterDescribe(RTSPClient rtspClient, int resultCode, string resultString) { while (true) { var scs = ((OurRtspClient)rtspClient).scs; if (resultCode != 0) { Console.Error.WriteLine ("Failed to get a SDP description: " + resultString); break; } var description = resultString; Console.Error.WriteLine ("Got a SDP description:"); Console.Error.Write (description); scs.session = MediaSession.CreateNew (env, description); if (scs.session == null) { Console.Error.WriteLine ("Failed to create a MediaSession object from the SDP description: " + env.GetResultMsg); break; } if (scs.session.HasSubsessions == 0) { Console.Error.WriteLine ("This session has no media subsessions (i.e., no \"m=\" lines)"); break; } scs.iter = new MediaSubsessionIterator (scs.session); SetupNextSubsession (rtspClient); return; } ShutdownStream (rtspClient); }
private void SetupNextSubsession(RTSPClient rtspClient) { var scs = ((OurRtspClient)rtspClient).scs; // alias scs.subsession = scs.iter.Next(); if (scs.subsession != null) { if (scs.subsession.Initiate(0) == 0) { Console.Error.WriteLine("Failed to initiate the \"" + scs.subsession + "\" subsession: " + env.GetResultMsg); SetupNextSubsession(rtspClient); // give up on this subsession; go to the next one } else { Console.Error.WriteLine("Initiated the \"" + scs.subsession + "\" subsession (client ports " + scs.subsession.ClientPortNum + "-" + (scs.subsession.ClientPortNum + 1) + ")"); // Continue setting up this subsession, by sending a RTSP "SETUP" command: rtspClient.SendSetupCommand(scs.subsession, ContinueAfterSetup, 0, 0, 0, null); } return; } // We've finished setting up all of the subsessions. Now, send a RTSP "PLAY" command to start the streaming: scs.duration = scs.session.PlayEndTime() - scs.session.PlayStartTime(); rtspClient.SendPlayCommand(scs.session, ContinueAfterPlay, 0, 0, 0, null); }
private void ShutdownStream(RTSPClient rtspClient, int exitCode = 1) { var scs = ((OurRtspClient)rtspClient).scs; // alias if (scs.session != null) { var someSubsessionsWereActive = false; var iter = new MediaSubsessionIterator(scs.session); MediaSubsession subsession; while ((subsession = iter.Next()) != null) { if (subsession.sink != null) { Medium.Close(subsession.sink); subsession.sink = null; if (subsession.RtcpInstance() != null) { subsession.RtcpInstance().SetByeHandler(null, IntPtr.Zero, 1); // in case the server sends a RTCP "BYE" while handling "TEARDOWN" } someSubsessionsWereActive = true; } } if (someSubsessionsWereActive) { // Send a RTSP "TEARDOWN" command, to tell the server to shutdown the stream. // Don't bother handling the response to the "TEARDOWN". rtspClient.SendTeardownCommand(scs.session, null, null); } } Console.Error.WriteLine(rtspClient + "Closing the stream."); Medium.Close(rtspClient); // Note that this will also cause this stream's "StreamClientState" structure to get reclaimed. if (--rtspClientCount == 0) { // The final stream has ended, so exit the application now. // (Of course, if you're embedding this code into your own application, you might want to comment this out.) Environment.Exit(exitCode); // If you choose *not* to "exit()" the application (i.e., if you comment out the call to "exit()" above), // and you don't intend to do anything more with this "TaskScheduler" and "UsageEnvironment", // then you can also reclaim the (small) memory used by these objects by doing the following. // (However, you must not do this until after you have left the event loop.) /* * TaskScheduler* scheduler = &(env.taskScheduler()); * env.reclaim(); * delete scheduler; */ } }
private void subsessionByeHandler(IntPtr clientData) { var subsession = new MediaSubsession(clientData); var rtspClient = new RTSPClient(subsession.miscPtr); Console.Error.WriteLine(rtspClient + "Received RTCP \"BYE\" on \"" + subsession + "\" subsession"); // Now act as if the subsession had closed: SubsessionAfterPlaying(subsession.Native.Native); }
private void ContinueAfterSetup(RTSPClient rtspClient, int resultCode, string resultString) { do { var scs = ((OurRtspClient)rtspClient).scs; // alias if (resultCode != 0) { Console.Error.WriteLine(rtspClient + "Failed to set up the \"" + scs.subsession + "\" subsession: " + env.GetResultMsg); break; } Console.Error.WriteLine(rtspClient + "Set up the \"" + scs.subsession + "\" subsession (client ports " + scs.subsession.ClientPortNum + "-" + (scs.subsession.ClientPortNum + 1) + ")"); // Having successfully setup the subsession, create a data sink for it, and call "startPlaying()" on it. // (This will prepare the data sink to receive data; the actual flow of data from the client won't start happening until later, // after we've sent a RTSP "PLAY" command.) scs.subsession.sink = DummySink.CreateNew(env, scs.subsession, rtspClient.Url); // perhaps use your own custom "MediaSink" subclass instead if (scs.subsession.sink == null) { Console.Error.WriteLine(rtspClient + "Failed to create a data sink for the \"" + scs.subsession + "\" subsession: " + env.GetResultMsg); break; } Console.Error.WriteLine(rtspClient + "Created a data sink for the \"" + scs.subsession + "\" subsession"); scs.subsession.miscPtr = rtspClient.Native.Native; // a hack to let subsession handle functions get the "RTSPClient" from the subsession scs.subsession.sink.StartPlaying(scs.subsession.ReadSource(), SubsessionAfterPlaying, scs.subsession.Native.Native); // Also set a handler to be called if a RTCP "BYE" arrives for this subsession: if (scs.subsession.RtcpInstance() != null) { scs.subsession.RtcpInstance().SetByeHandler(subsessionByeHandler, scs.subsession.Native.Native, 1); } } while (false); // Set up the next subsession, if any: SetupNextSubsession(rtspClient); }
public void Start(string commandData) { try { m_rtspAppStruct = new RTSPAppStruct(commandData, false); RTSPClient rtspClient = new RTSPClient(); string sdp = rtspClient.GetStreamDescription(m_rtspAppStruct.URL); SIPResponse sipResponse = GetOkResponse(m_clientTransaction.TransactionRequest, sdp); m_clientTransaction.SendFinalResponse(sipResponse); //rtspClient.Start(m_rtspAppStruct.URL, m_clientTransaction.RemoteEndPoint.Address, m_rtpPort); } catch (Exception excp) { logger.Error("Exception RTSPCall StartCall. " + excp.Message); } }
private void ContinueAfterDescribe(RTSPClient rtspClient, int resultCode, string resultString) { while (true) { var scs = ((OurRtspClient)rtspClient).scs; if (resultCode != 0) { Console.Error.WriteLine("Failed to get a SDP description: " + resultString); break; } var description = resultString; Console.Error.WriteLine("Got a SDP description:"); Console.Error.Write(description); scs.session = MediaSession.CreateNew(env, description); if (scs.session == null) { Console.Error.WriteLine("Failed to create a MediaSession object from the SDP description: " + env.GetResultMsg); break; } if (scs.session.HasSubsessions == 0) { Console.Error.WriteLine("This session has no media subsessions (i.e., no \"m=\" lines)"); break; } scs.iter = new MediaSubsessionIterator(scs.session); SetupNextSubsession(rtspClient); return; } ShutdownStream(rtspClient); }
private void SubsessionAfterPlaying(IntPtr clientData) { var subsession = new MediaSubsession(clientData); var rtspClient = new RTSPClient(subsession.miscPtr); // Begin by closing this subsession's stream: Medium.Close(subsession.sink); subsession.sink = null; // Next, check whether *all* subsessions' streams have now been closed: var session = subsession.ParentSession(); var iter = new MediaSubsessionIterator(session); while ((subsession = iter.Next()) != null) { if (subsession.sink != null) { return; // this subsession is still active } } // All subsessions' streams have now been closed, so shutdown the client: ShutdownStream(rtspClient); }
public void Connect() { byte [] rbs = new byte[4 + 8 + 16]; RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider(); rng.GetBytes(rbs); string sid = String.Format("{0:D10}", BitConverter.ToUInt32(rbs, 0)); string sci = String.Format("{0:X16}", BitConverter.ToUInt64(rbs, 4)); string sac = Convert.ToBase64String(rbs, 12, 16); string url = String.Format("rtsp://{0}/{1}", local, sid); rc = new RTSPClient(host, 5000, url); rc.UserAgent = "iTunes/4.6 (Macintosh; U; PPC Mac OS X 10.3)"; rc.AddHeaders.Set("Client-Instance", sci); rc.Connect(); string key = Convert.ToBase64String(RSAEncrypt(alg.Key)); string iv = Convert.ToBase64String(alg.IV); string sdp = String.Format( "v=0\r\n" + "o=iTunes {0} 0 IN IP4 {1}\r\n" + "s=iTunes\r\n" + "c=IN IP4 {2}\r\n" + "t=0 0\r\n" + "m=audio 0 RTP/AVP 96\r\n" + "a=rtpmap:96 AppleLossless\r\n" + "a=fmtp:96 4096 0 16 40 10 14 2 255 0 0 44100\r\n" + "a=rsaaeskey:{3}\r\n" + "a=aesiv:{4}\r\n", sid, local, host, key.Replace("=", ""), iv.Replace("=", "")); rc.AddHeaders.Set("Apple-Challenge", sac.Replace("=", "")); rc.AnnounceSDP(sdp); rc.AddHeaders.Remove("Apple-Challenge"); Hashtable ht = rc.Setup(); string aj = (string)ht["Audio-Jack-Status"]; if (aj == null) { throw new Exception("Audio-Jack-Status is missing"); } string [] ptokens = aj.Split(new char[] { ';' }); for (int i = 0; i < ptokens.Length; i++) { string [] ctokens = ptokens[i].Split(new char[] { '=' }); for (int j = 0; j < ctokens.Length; j++) { if (ctokens.Length == 1 && ctokens[0].Trim().Equals("connected")) { ajstatus = JACK_STATUS_CONNECTED; } else if (ctokens.Length == 2 && ctokens[0].Trim().Equals("type")) { if (ctokens[1].Trim().Equals("digital")) { ajtype = JACK_TYPE_DIGITAL; } } } } rc.Record(); UpdateVolume(); tcdata = new TcpClient(); tcdata.Connect(host, rc.ServerPort); nsdata = tcdata.GetStream(); }
private void subsessionByeHandler(IntPtr clientData) { var subsession = new MediaSubsession (clientData); var rtspClient = new RTSPClient (subsession.miscPtr); Console.Error.WriteLine (rtspClient + "Received RTCP \"BYE\" on \"" + subsession + "\" subsession"); // Now act as if the subsession had closed: SubsessionAfterPlaying (subsession.Native.Native); }
private void SubsessionAfterPlaying(IntPtr clientData) { var subsession = new MediaSubsession (clientData); var rtspClient = new RTSPClient (subsession.miscPtr); // Begin by closing this subsession's stream: Medium.Close (subsession.sink); subsession.sink = null; // Next, check whether *all* subsessions' streams have now been closed: var session = subsession.ParentSession (); var iter = new MediaSubsessionIterator (session); while ((subsession = iter.Next ()) != null) { if (subsession.sink != null) return; // this subsession is still active } // All subsessions' streams have now been closed, so shutdown the client: ShutdownStream (rtspClient); }
private void SetupNextSubsession(RTSPClient rtspClient) { var scs = ((OurRtspClient)rtspClient).scs; // alias scs.subsession = scs.iter.Next (); if (scs.subsession != null) { if (scs.subsession.Initiate (0) == 0) { Console.Error.WriteLine ("Failed to initiate the \"" + scs.subsession + "\" subsession: " + env.GetResultMsg); SetupNextSubsession (rtspClient); // give up on this subsession; go to the next one } else { Console.Error.WriteLine ("Initiated the \"" + scs.subsession + "\" subsession (client ports " + scs.subsession.ClientPortNum + "-" + (scs.subsession.ClientPortNum + 1) + ")"); // Continue setting up this subsession, by sending a RTSP "SETUP" command: rtspClient.SendSetupCommand (scs.subsession, ContinueAfterSetup, 0, 0, 0, null); } return; } // We've finished setting up all of the subsessions. Now, send a RTSP "PLAY" command to start the streaming: scs.duration = scs.session.PlayEndTime () - scs.session.PlayStartTime (); rtspClient.SendPlayCommand (scs.session, ContinueAfterPlay, 0, 0, 0, null); }
private void ContinueAfterSetup(RTSPClient rtspClient, int resultCode, string resultString) { do { var scs = ((OurRtspClient)rtspClient).scs; // alias if (resultCode != 0) { Console.Error.WriteLine (rtspClient + "Failed to set up the \"" + scs.subsession + "\" subsession: " + env.GetResultMsg); break; } Console.Error.WriteLine (rtspClient + "Set up the \"" + scs.subsession + "\" subsession (client ports " + scs.subsession.ClientPortNum + "-" + (scs.subsession.ClientPortNum + 1) + ")"); // Having successfully setup the subsession, create a data sink for it, and call "startPlaying()" on it. // (This will prepare the data sink to receive data; the actual flow of data from the client won't start happening until later, // after we've sent a RTSP "PLAY" command.) scs.subsession.sink = DummySink.CreateNew (env, scs.subsession, rtspClient.Url); // perhaps use your own custom "MediaSink" subclass instead if (scs.subsession.sink == null) { Console.Error.WriteLine (rtspClient + "Failed to create a data sink for the \"" + scs.subsession + "\" subsession: " + env.GetResultMsg); break; } Console.Error.WriteLine (rtspClient + "Created a data sink for the \"" + scs.subsession + "\" subsession"); scs.subsession.miscPtr = rtspClient.Native.Native; // a hack to let subsession handle functions get the "RTSPClient" from the subsession scs.subsession.sink.StartPlaying (scs.subsession.ReadSource (), SubsessionAfterPlaying, scs.subsession.Native.Native); // Also set a handler to be called if a RTCP "BYE" arrives for this subsession: if (scs.subsession.RtcpInstance () != null) { scs.subsession.RtcpInstance ().SetByeHandler (subsessionByeHandler, scs.subsession.Native.Native, 1); } } while (false); // Set up the next subsession, if any: SetupNextSubsession (rtspClient); }
public void Connect() { byte [] rbs = new byte[ 4 + 8 + 16 ]; RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider(); rng.GetBytes( rbs ); string sid = String.Format( "{0:D10}", BitConverter.ToUInt32( rbs, 0 ) ); string sci = String.Format( "{0:X16}", BitConverter.ToUInt64( rbs, 4 ) ); string sac = Convert.ToBase64String( rbs, 12, 16 ); string url = String.Format( "rtsp://{0}/{1}", local, sid ); rc = new RTSPClient( host, 5000, url ); rc.UserAgent = "iTunes/4.6 (Macintosh; U; PPC Mac OS X 10.3)"; rc.AddHeaders.Set( "Client-Instance", sci ); rc.Connect(); string key = Convert.ToBase64String( RSAEncrypt( alg.Key ) ); string iv = Convert.ToBase64String( alg.IV ); string sdp = String.Format( "v=0\r\n" + "o=iTunes {0} 0 IN IP4 {1}\r\n" + "s=iTunes\r\n" + "c=IN IP4 {2}\r\n" + "t=0 0\r\n" + "m=audio 0 RTP/AVP 96\r\n" + "a=rtpmap:96 AppleLossless\r\n" + "a=fmtp:96 4096 0 16 40 10 14 2 255 0 0 44100\r\n" + "a=rsaaeskey:{3}\r\n" + "a=aesiv:{4}\r\n", sid, local, host, key.Replace( "=", "" ), iv.Replace( "=", "" ) ); rc.AddHeaders.Set( "Apple-Challenge", sac.Replace( "=", "" ) ); rc.AnnounceSDP( sdp ); rc.AddHeaders.Remove( "Apple-Challenge" ); Hashtable ht = rc.Setup(); string aj = (string)ht[ "Audio-Jack-Status" ]; if( aj == null ) throw new Exception( "Audio-Jack-Status is missing" ); string [] ptokens = aj.Split( new char[] { ';' } ); for( int i = 0; i < ptokens.Length; i++ ) { string [] ctokens = ptokens[ i ].Split( new char[] { '=' } ); for( int j = 0; j < ctokens.Length; j++ ) { if( ctokens.Length == 1 && ctokens[ 0 ].Trim().Equals( "connected" ) ) { ajstatus = JACK_STATUS_CONNECTED; } else if( ctokens.Length == 2 && ctokens[ 0 ].Trim().Equals( "type" ) ) { if( ctokens[ 1 ].Trim().Equals( "digital" ) ) ajtype = JACK_TYPE_DIGITAL; } } } rc.Record(); UpdateVolume(); tcdata = new TcpClient(); tcdata.Connect( host, rc.ServerPort ); nsdata = tcdata.GetStream(); }
public async void rtspClientStart() { rtspCancel = new CancellationTokenSource(); var url = "rtsp://192.168.0.10:8554/H264Video"; String now = DateTime.Now.ToString("yyyyMMdd_HHmmss"); MemoryStream fs_vps = null; // used to write the video MemoryStream fs_v = null; // used to write the video MemoryStream fs_a = null; // used to write the audio h264 = false; h265 = false; bool spsdone = false; RTSPClient c = new RTSPClient(); // The SPS/PPS comes from the SDP data // or it is the first SPS/PPS from the H264 video stream c.Received_SPS_PPS += (byte[] sps, byte[] pps) => { h264 = true; if (fs_vps == null) { String filename = "rtsp_capture_" + now + ".264"; fs_vps = new MemoryStream(); } if (fs_vps != null) { fs_vps.SetLength(0); fs_vps.Write(new byte[] { 0x00, 0x00, 0x00, 0x01 }, 0, 4); // Write Start Code fs_vps.Write(sps, 0, sps.Length); fs_vps.Write(new byte[] { 0x00, 0x00, 0x00, 0x01 }, 0, 4); // Write Start Code fs_vps.Write(pps, 0, pps.Length); } }; c.Received_VPS_SPS_PPS += (byte[] vps, byte[] sps, byte[] pps) => { h265 = true; if (fs_vps == null) { String filename = "rtsp_capture_" + now + ".265"; fs_vps = new MemoryStream(); } if (fs_vps != null) { fs_vps.SetLength(0); fs_vps.Write(new byte[] { 0x00, 0x00, 0x00, 0x01 }, 0, 4); // Write Start Code fs_vps.Write(vps, 0, vps.Length); // Video parameter set fs_vps.Write(new byte[] { 0x00, 0x00, 0x00, 0x01 }, 0, 4); // Write Start Code fs_vps.Write(sps, 0, sps.Length); // Sequence Parameter Set fs_vps.Write(new byte[] { 0x00, 0x00, 0x00, 0x01 }, 0, 4); // Write Start Code fs_vps.Write(pps, 0, pps.Length); // Picture Parameter Set } }; // Video NALs. May also include the SPS and PPS in-band for H264 c.Received_NALs += (List <byte[]> nal_units) => { foreach (byte[] nal_unit in nal_units) { // Output some H264 stream information if (h264 && nal_unit.Length > 0) { int nal_ref_idc = (nal_unit[0] >> 5) & 0x03; int nal_unit_type = nal_unit[0] & 0x1F; String description = ""; if (nal_unit_type == 1) { description = "NON IDR NAL"; } else if (nal_unit_type == 5) { description = "IDR NAL"; } else if (nal_unit_type == 6) { description = "SEI NAL"; } else if (nal_unit_type == 7) { description = "SPS NAL"; } else if (nal_unit_type == 8) { description = "PPS NAL"; } else if (nal_unit_type == 9) { description = "ACCESS UNIT DELIMITER NAL"; } else { description = "OTHER NAL"; } //Console.WriteLine("NAL Ref = " + nal_ref_idc + " NAL Type = " + nal_unit_type + " " + description); } // Output some H265 stream information if (h265 && nal_unit.Length > 0) { int nal_unit_type = (nal_unit[0] >> 1) & 0x3F; String description = ""; if (nal_unit_type == 1) { description = "NON IDR NAL"; } else if (nal_unit_type == 19) { description = "IDR NAL"; } else if (nal_unit_type == 32) { description = "VPS NAL"; } else if (nal_unit_type == 33) { description = "SPS NAL"; } else if (nal_unit_type == 34) { description = "PPS NAL"; } else if (nal_unit_type == 39) { description = "SEI NAL"; } else { description = "OTHER NAL"; } //Console.WriteLine("NAL Type = " + nal_unit_type + " " + description); } // we need sps... first if (!h264 && !h265) { return; } if (!spsdone) { if (callbacks == null || callbacks.buffers.Count == 0) { return; } var index = callbacks.buffers.Pop(); var buffer = codec.GetInputBuffer(index); buffer.Clear(); buffer.Put(fs_vps.ToArray()); codec.QueueInputBuffer(index, 0, (int)fs_vps.Length, 0, MediaCodecBufferFlags.CodecConfig); spsdone = true; fs_v = new MemoryStream(); } if (fs_v != null) { fs_v.Write(new byte[] { 0x00, 0x00, 0x00, 0x01 }, 0, 4); // Write Start Code fs_v.Write(nal_unit, 0, nal_unit.Length); // Write NAL } if (callbacks == null || fs_v == null || callbacks.buffers.Count == 0) { return; } try { var index = callbacks.buffers.Pop(); var buffer = codec.GetInputBuffer(index); buffer.Clear(); buffer.Put(fs_v.ToArray()); codec.QueueInputBuffer(index, 0, (int)fs_v.Length, 0, MediaCodecBufferFlags.None); fs_v.SetLength(0); } catch { } } }; // seperate and stay running Task.Run(() => { while (rtspCancel != null && true) { try { if (rtspCancel.Token.IsCancellationRequested) { return; } c.Connect(url, RTSPClient.RTP_TRANSPORT.UDP); var lastrtp = 0; int cnt = 0; while (!c.StreamingFinished()) { rtsprunning = true; Thread.Sleep(500); // existing if (rtspCancel.Token.IsCancellationRequested) { c.Stop(); return; } // no rtp in .5 sec if (lastrtp == c.rtp_count && cnt++ > 5) { c.Stop(); rtspCancel = null; return; } lastrtp = c.rtp_count; } rtsprunning = false; } catch { } } }); }
private void ShutdownStream(RTSPClient rtspClient, int exitCode = 1) { var scs = ((OurRtspClient)rtspClient).scs; // alias if (scs.session != null) { var someSubsessionsWereActive = false; var iter = new MediaSubsessionIterator (scs.session); MediaSubsession subsession; while ((subsession = iter.Next ()) != null) { if (subsession.sink != null) { Medium.Close (subsession.sink); subsession.sink = null; if (subsession.RtcpInstance () != null) { subsession.RtcpInstance ().SetByeHandler (null, IntPtr.Zero, 1); // in case the server sends a RTCP "BYE" while handling "TEARDOWN" } someSubsessionsWereActive = true; } } if (someSubsessionsWereActive) { // Send a RTSP "TEARDOWN" command, to tell the server to shutdown the stream. // Don't bother handling the response to the "TEARDOWN". rtspClient.SendTeardownCommand (scs.session, null, null); } } Console.Error.WriteLine (rtspClient + "Closing the stream."); Medium.Close (rtspClient); // Note that this will also cause this stream's "StreamClientState" structure to get reclaimed. if (--rtspClientCount == 0) { // The final stream has ended, so exit the application now. // (Of course, if you're embedding this code into your own application, you might want to comment this out.) Environment.Exit (exitCode); // If you choose *not* to "exit()" the application (i.e., if you comment out the call to "exit()" above), // and you don't intend to do anything more with this "TaskScheduler" and "UsageEnvironment", // then you can also reclaim the (small) memory used by these objects by doing the following. // (However, you must not do this until after you have left the event loop.) /* TaskScheduler* scheduler = &(env.taskScheduler()); env.reclaim(); delete scheduler; */ } }