├── .gitignore ├── LICENSE ├── RTSPTest.java ├── README.md └── RTSPControl.java /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | 3 | # Mobile Tools for Java (J2ME) 4 | .mtj.tmp/ 5 | 6 | # Package Files # 7 | *.jar 8 | *.war 9 | *.ear 10 | 11 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 12 | hs_err_pid* 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Raymond Phan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /RTSPTest.java: -------------------------------------------------------------------------------- 1 | import java.awt.BorderLayout; 2 | import java.awt.Dimension; 3 | import java.awt.GridLayout; 4 | import java.awt.event.ActionEvent; 5 | import java.awt.event.ActionListener; 6 | import java.awt.event.WindowAdapter; 7 | import java.awt.event.WindowEvent; 8 | 9 | import javax.swing.JButton; 10 | import javax.swing.JFrame; 11 | import javax.swing.JPanel; 12 | import javax.swing.JTextField; 13 | 14 | public class RTSPTest implements ActionListener{ 15 | // GUI 16 | private JFrame mainFrame; 17 | private JButton setupButton; 18 | private JButton playButton; 19 | private JButton pauseButton; 20 | private JButton optionsButton; 21 | private JButton describeButton; 22 | private JButton teardownButton; 23 | private JTextField textField; 24 | 25 | // Panel to place our push buttons 26 | private JPanel buttonPanel; 27 | 28 | // Panel to place our text field panel 29 | private JPanel textPanel; 30 | 31 | // Panel to place all of our objects in one 32 | private JPanel mainPanel; 33 | 34 | // Our RTSP Object 35 | private RTSPControl rtspControl; 36 | 37 | private String hostName; 38 | private int portNumber; 39 | private String videoFile; 40 | 41 | public RTSPTest() { 42 | // Set up our GUI elements 43 | mainFrame = new JFrame("RTSP Test"); 44 | 45 | setupButton = new JButton("Setup"); 46 | playButton = new JButton("Play"); 47 | pauseButton = new JButton("Pause"); 48 | optionsButton = new JButton("Options"); 49 | describeButton = new JButton("Describe"); 50 | teardownButton = new JButton("Teardown"); 51 | textField = new JTextField("rtsp://184.72.239.149/vod/mp4:BigBuckBunny_115k.mov"); 52 | 53 | buttonPanel = new JPanel(); 54 | textPanel = new JPanel(); 55 | mainPanel = new JPanel(); 56 | 57 | // Set up buttons 58 | buttonPanel.setLayout(new GridLayout(1,0)); 59 | buttonPanel.add(describeButton); 60 | buttonPanel.add(optionsButton); 61 | buttonPanel.add(setupButton); 62 | buttonPanel.add(playButton); 63 | buttonPanel.add(pauseButton); 64 | buttonPanel.add(teardownButton); 65 | 66 | setupButton.addActionListener(this); 67 | playButton.addActionListener(this); 68 | pauseButton.addActionListener(this); 69 | optionsButton.addActionListener(this); 70 | describeButton.addActionListener(this); 71 | teardownButton.addActionListener(this); 72 | textField.addActionListener(this); 73 | 74 | // Set up text field 75 | textPanel.setLayout(new GridLayout(1,0)); 76 | textPanel.add(textField); 77 | 78 | // Set main panel 79 | mainPanel.setLayout(null); 80 | mainPanel.add(textPanel); 81 | mainPanel.add(buttonPanel); 82 | textPanel.setBounds(0, 0, 450, 25); 83 | buttonPanel.setBounds(0, 25, 450, 50); 84 | 85 | // Add listener so that we quit we close the window 86 | mainFrame.addWindowListener(new WindowAdapter() { 87 | public void windowClosing(WindowEvent e) { 88 | // Teardown the connection when we close the window 89 | if (rtspControl != null) { 90 | rtspControl.RTSPTeardown(); 91 | rtspControl.resetParameters(); // Just in case 92 | } 93 | System.exit(0); 94 | } 95 | }); 96 | 97 | // Set up connection to default RTSP url before we even show GUI 98 | //hostName = "184.72.239.149"; 99 | //portNumber = 554; 100 | //videoFile = "vod/mp4:BigBuckBunny_115k.mov"; 101 | //rtspControl = new RTSPControl(hostName, portNumber, videoFile); 102 | 103 | // Create frame and show 104 | mainFrame.getContentPane().add(mainPanel, BorderLayout.CENTER); 105 | mainFrame.setSize(new Dimension(450, 100)); 106 | mainFrame.setVisible(true); 107 | } 108 | 109 | @Override 110 | public void actionPerformed(ActionEvent e) { 111 | if (e.getSource() == setupButton) { 112 | System.out.println("Setup Button Pressed!"); 113 | if (rtspControl == null) { 114 | System.out.println("RTSP Object has not been created!"); 115 | return; 116 | } 117 | // Keep issuing setup commands as long as there are tracks 118 | // to set up 119 | while (rtspControl.RTSPSetup() > 0); 120 | //int numberTracksLeft = rtspControl.RTSPSetup(); 121 | //if (numberTracksLeft > 0) 122 | // System.out.println("There are " + numberTracksLeft + " tracks left to set up"); 123 | } 124 | else if (e.getSource() == describeButton) { 125 | System.out.println("Describe Button Pressed!"); 126 | if (rtspControl == null) { 127 | System.out.println("RTSP Object has not been created!"); 128 | return; 129 | } 130 | rtspControl.RTSPDescribe(); 131 | } 132 | else if (e.getSource() == optionsButton) { 133 | System.out.println("Options Button Pressed!"); 134 | if (rtspControl == null) { 135 | System.out.println("RTSP Object has not been created!"); 136 | return; 137 | } 138 | rtspControl.RTSPOptions(); 139 | } 140 | else if (e.getSource() == playButton) { 141 | System.out.println("Play Button Pressed!"); 142 | if (rtspControl == null) { 143 | System.out.println("RTSP Object has not been created!"); 144 | return; 145 | } 146 | rtspControl.RTSPPlay(); 147 | } 148 | else if (e.getSource() == pauseButton) { 149 | System.out.println("Pause Button Pressed!"); 150 | if (rtspControl == null) { 151 | System.out.println("RTSP Object has not been created!"); 152 | return; 153 | } 154 | rtspControl.RTSPPause(); 155 | } 156 | else if (e.getSource() == teardownButton) { 157 | System.out.println("Teardown Button Pressed!"); 158 | if (rtspControl == null) { 159 | System.out.println("RTSP Object has not been created!"); 160 | return; 161 | } 162 | rtspControl.RTSPTeardown(); 163 | } 164 | else if (e.getSource() == textField) { 165 | // Grab the text from the field 166 | String rtspURL = textField.getText(); 167 | System.out.println("String entered in text field: " + rtspURL); 168 | 169 | rtspControl = new RTSPControl(rtspURL); 170 | 171 | hostName = rtspControl.getRTSPURL(); 172 | System.out.println("URL: " + hostName); 173 | portNumber = rtspControl.getServerPort(); 174 | System.out.println("Port Number: " + portNumber); 175 | videoFile = rtspControl.getVideoFilename(); 176 | System.out.println("Video Filename: " + videoFile); 177 | } 178 | } 179 | 180 | public static void main(String[] args) { 181 | new RTSPTest(); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | RTSPTest 2 | ======== 3 | 4 | This is a library as well as a test GUI that I wrote that implements the RTSP Signalling Protocol to communicate with RTSP servers 5 | 6 | **Author: Raymond Phan - `rphan@ryerson.ca`** 7 | 8 | ## Version History 9 | 10 | * Version 1.0 - June 3rd, 2014 - Initial Creation 11 | * Version 1.1 - August 25th, 2015 - Fix on searching for track IDs (thanks to [`@galbarm`](https://github.com/galbarm)) 12 | * Version 1.2 - February 1st, 2016 - Further fix on searching for track IDs (thanks to [`@abhijeetbhanjadeo`](https://github.com/abhijeetbhanjadeo)) 13 | 14 | # Synopsis 15 | 16 | This library is written in Java. There are two classes associated with this repo: 17 | 18 | 1. `RTSPControl` - The RTSP Protocol class that communicates with the server that establishes a connection, and eventually get media packets for the data file that we want. 19 | 2. `RTSPTest` - A simple Java Swing application that allows you to enter in a RTSP URL as well as push buttons that allow you to communicate with the server. 20 | 21 | # Introduction 22 | 23 | The Real-Time Streaming Protocol (RTSP) is a method to issue commands to a server that implements the Real-Time Protocol (RTP). An RTP server sends data over to a receiving entity in a particular format dictated by the RTP protocol. As such, RTSP is primarily used to communicate the wishes of the user / device, such as play, pause, teardown, setup and so on. Once the setup is complete and a play command is issued, the server thus sends the relevant data using RTP to the device. 24 | 25 | As such, this is code that was written that implements the RTSP protocol to communicate with servers that stream data to devices via the RTP protocol. In order to connect to an RTSP server, the URL is in the following format: 26 | 27 | rtsp://address.of.server:port/pathToFileToPlay 28 | 29 | `rtsp` signifies that this is an RTSP server we wish to communicate with. `address.of.server` is the IP address of the RTSP server. This can also be a DNS issued name (ex: google.com, bublcam.com, etc.) that will eventually resolve to the IP address of the server we want. `port` is the port we wish to access on the server. The default port for RTSP/RTP is usually 554. `pathToFileToPlay` is the exact path of where the content you want to play is stored. The `pathToFileToPlay` is server dependent and so you will need to double check on how this is structured before you try and access the file you want to play. As an example, here is a valid RTSP URL at the time of this posting. This is a RTSP test server provided by Wowza Media Systems: 30 | 31 | rtsp://184.72.239.149/vod/mp4:BigBuckBunny_115k.mov 32 | 33 | As such, the IP address is `184.72.239.149`, there is no port specified, so the default is `554`. The video we wish to play is stored in `vod/mp4:BigBuckBunny_115k.mov`. Bear in mind that the `:` is not a valid character to use in all operating systems in terms of directory structure. However, Wowza has their own parsing scheme using `:` that will inevitably determine where the exact file is. As such, it is paramount that you know how exactly to get to the media you want by knowing how to correctly structure the `pathToFileToPlay`. 34 | 35 | # Basic RTSP Actions 36 | 37 | There are 6 basic actions that a user may wish to send to the server, and there are other minor ones but for the sake of this README and for simplicity, we will ignore those. These are: 38 | 39 | * DESCRIBE 40 | * OPTIONS 41 | * SETUP 42 | * PLAY 43 | * PAUSE 44 | * TEARDOWN 45 | 46 | ## DESCRIBE 47 | 48 | The purpose of `DESCRIBE` is to be able to *describe* the information about the media that you are trying to stream. This includes what codecs / protocols the audio and video is being used for the content, what kind of streaming protocols are used, and it also displays other information about you accessing the content. This is returned in `SDP` format, or Session Description Protocol. An example of what happens after you send a `DESCRIBE` request could look like this: 49 | 50 | RTSP/1.0 200 OK 51 | Content-Base: rtsp://184.72.239.149:554/vod/mp4:BigBuckBunny_115k.mov/ 52 | Date: Mon, 2 Jun 2014 21:06:45 UTC 53 | Content-Length: 587 54 | Session: 435048744;timeout=60 55 | Expires: Mon, 2 Jun 2014 21:06:45 UTC 56 | Cseq: 1 57 | Content-Type: application/sdp 58 | Server: Wowza Media Server 3.6.4.04 build11295 59 | Cache-Control: no-cache 60 | 61 | v=0 62 | o=- 435048744 435048744 IN IP4 184.72.239.149 63 | s=BigBuckBunny_115k.movs 64 | c=IN IP4 184.72.239.149 65 | t=0 0 66 | a=sdplang:en 67 | a=range:npt=0- 596.48 68 | a=control:* 69 | m=audio 0 RTP/AVP 96 70 | a=rtpmap:96 mpeg4-generic/12000/2 71 | a=fmtp:96 profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1490 72 | a=control:trackID=1 73 | m=video 0 RTP/AVP 97 74 | a=rtpmap:97 H264/90000 75 | a=fmtp:97 packetization-mode=1;profile-level-id=42C01E;sprop-parameter-sets=Z0LAHtkDxWhAAAADAEAAAAwDxYuS,aMuMsg== 76 | a=cliprect:0,0,160,240 77 | a=framesize:97 240-160 78 | a=framerate:24.0 79 | a=control:trackID=2 80 | 81 | Basically, the first chunk of text gives you information about when you have accessed the data as well as some general information about it. What is important is the very first line: `RTSP/1.0 200 OK`. A server response code of `200` means that everything is working fine. Anything else you should consider it as being an error. The second chunk of text gives you information about the media you are trying to access. What is important are the `m=video` and `m=audio` tags. What follows each of these tags are information about the audio or video data within the content you are trying to stream. As you can see, the video standard used for the content is H.264, while the audio is MP4. It is also important to see that where the tags `a=control` are, this tells you the `trackID` or basically which "channel" we need to access either the audio or video data. More information about the SDP protocol can [be found here](http://en.wikipedia.org/wiki/Session_Description_Protocol). Another way that this can be specified is if there is a `stream` tag instead of `trackID`. This is essentially the same thing. There is more important information about the video and audio tracks, but the current implementation does not extract this information. On future versions, this information will inevitably be extracted to allow for actual media playback. 82 | 83 | ## OPTIONS 84 | 85 | This pretty much tells you what *commands* are available to you to send to the server. When you send an `OPTIONS` command, the server will return something like this: 86 | 87 | RTSP/1.0 200 OK 88 | Supported: play.basic, con.persistent 89 | Cseq: 1 90 | Server: Wowza Media Server 3.6.4.04 build11295 91 | Public: DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, OPTIONS, ANNOUNCE, RECORD, GET_PARAMETER 92 | Cache-Control: no-cache 93 | 94 | What is important is where the `Public:` field is. These are all of the available commands that we can issue to the server. As you can see, the 6 big ones that are in the list above are in this chunk of text. 95 | 96 | ## SETUP 97 | 98 | What the `SETUP` command does is that we need to establish a connection to the server so that we can access each "channel" of data (audio and video). As such, we need one `SETUP` command for audio and one for video. What this may look like for audio and video that's returned from the server could be: 99 | 100 | RTSP/1.0 200 OK 101 | Date: Mon, 2 Jun 2014 23:59:19 UTC 102 | Transport: RTP/AVP;unicast;client_port=9000-9001;source=184.72.239.149;server_port=8224-8225;ssrc=445F8F19 103 | Session: 1406752451;timeout=60 104 | Expires: Mon, 2 Jun 2014 23:59:19 UTC 105 | Cseq: 2 106 | Server: Wowza Media Server 3.6.4.04 build11295 107 | Cache-Control: no-cache 108 | 109 | RTSP/1.0 200 OK 110 | Date: Mon, 2 Jun 2014 23:59:19 UTC 111 | Transport: RTP/AVP;unicast;client_port=9000-9001;source=184.72.239.149;server_port=8230-8231;ssrc=652A0475 112 | Session: 1406752451;timeout=60 113 | Expires: Mon, 2 Jun 2014 23:59:19 UTC 114 | Cseq: 3 115 | Server: Wowza Media Server 3.6.4.04 build11295 116 | Cache-Control: no-cache 117 | 118 | Note that both of the string chunks are more or less the same. The **only** difference is the `server_port`. 119 | 120 | ## PLAY 121 | 122 | When we issue a `PLAY` command, this is when we start obtaining media packets. The server could return something like this: 123 | 124 | RTSP/1.0 200 OK 125 | Range: npt=0.0- 126 | Session: 1456101883;timeout=60 127 | Cseq: 4 128 | RTP-Info: url=rtsp://184.72.239.149:554/vod/mp4:BigBuckBunny_115k.mov/trackID=1;seq=1;rtptime=0,url=rtsp://184.72.239.149:554/vod/mp4:BigBuckBunny_115k.mov/trackID=2;seq=1;rtptime=0 129 | Server: Wowza Media Server 3.6.4.04 build11295 130 | Cache-Control: no-cache 131 | 132 | This simply displays what audio and video tracks we are trying to access. 133 | 134 | ## PAUSE 135 | 136 | When we issue a `PAUSE` command, we want to stop the server from sending media packets, but we will eventually push `PLAY` again. When issuing a `PLAY` command after the server is paused, it should resume playback and start right where you left off. The server could return something like this: 137 | 138 | RTSP/1.0 200 OK 139 | Session: 1456101883;timeout=60 140 | Cseq: 5 141 | Server: Wowza Media Server 3.6.4.04 build11295 142 | Cache-Control: no-cache 143 | 144 | This is pretty inconsequential. The server is just replying back with information about the current session. 145 | 146 | ## TEARDOWN 147 | 148 | We issue a `TEARDOWN` command when we are finished with the server and wish to terminate communication. After issuing a `TEARDOWN` command, the server could return something like: 149 | 150 | RTSP/1.0 200 OK 151 | Session: 1456101883;timeout=60 152 | Cseq: 6 153 | Server: Wowza Media Server 3.6.4.04 build11295 154 | Cache-Control: no-cache 155 | 156 | This returns the same information as the `PAUSE` command. We are essentially doing the same thing, but we are also terminating our connection with the server. 157 | 158 | # Usual Workflow of RTSP Commands 159 | 160 | The workflow on issuing RTSP commands to the server from start to finish is usually done in the following way: 161 | 162 | 1. Send a `DESCRIBE` or `OPTIONS` request - This is used to either figure out which track IDs store our audio and video streams, already figuring out what kinds of commands we can issue, or querying what kind of commands we have available. 163 | 2. If we did `OPTIONS` in Step #1, we now do `DESCRIBE`. 164 | 3. Issue a `SETUP` command to establish what kind of "channels" we wish to stream. 165 | 4. Issue a `PLAY` command, and we can alternate between `PAUSE` and `PLAY` at will. 166 | 5. Issue a `TEARDOWN` command when we have finished with the server. 167 | 168 | # How to issue RTSP Commands 169 | 170 | To communicate with the RTSP server, you need to create specifically formatted messages. The format is almost exactly like what the server returns to you with regards to the chunks of strings that we have seen before. All you need to do is write out the message in its character form, then send each character in byte form (ASCII codes). Each line of the message must have a manual carriage return (`\r`) followed by a new line (`\n`). You first need to establish a `TCP` connection with the server over the RTSP port (default is 554). Once you do this, you create a Datagram Socket using any port between (0 to 65535, with the exception of 554) so that we can receive media packets from the server when the time comes (i.e. issuing the `PLAY` command) at this port. Usually this port is at 9000 for the media data sent from the RTP server, with port 9001 to receive data via the Real-Time Control Protocol (RTCP). This data provides feedback on the quality of service (QoS) in media distribution by periodically sending statistics information to participants in a streaming multimedia session. 171 | 172 | Let's illustrate how these commands should be written in the same procedure as the workflow as what we have described before. 173 | 174 | ## DESCRIBE 175 | 176 | The `DESCRIBE` command is what is usually sent first. The way you structure the `DESCRIBE` command to be sent to the server is: 177 | 178 | DESCRIBE rtsp://184.72.239.149:554/vod/mp4:BigBuckBunny_115k.mov RTSP/1.0\r\n 179 | CSeq: 1\r\n 180 | \r\n 181 | 182 | The `\r\n` denote a carriage return and new line for each line. You would actually write this into the string chunk. The above is the **string** representation of what you would actually send to the server. Each character would be sent as one byte (converted into ASCII) over to the server. Bear in mind that these bytes have to be sent in big-endian (network byte order). The Java Virtual Machine already handles this for us so there is no need to do any byte re-ordering. Also take note of the `CSeq: 1` string. This is known as a **Command Sequence** number, which keeps track of how many commands we have sent to the server so far. As can be seen, this is just the first command. For each command we send, this number needs to be incremented by 1. 183 | 184 | When the `DESCRIBE` response is received from the server, a **Session** ID is issued. This **Session** ID needs to be part of subsequent messages sent to the server. This can consist of both letters and numbers. 185 | 186 | ## OPTIONS 187 | 188 | The `OPTIONS` command looks like the following. You can do either `DESCRIBE` or `OPTIONS` first. Let's assume that we have sent a `DESCRIBE` command first, and let's say our Session ID that we got back was 1234567890: 189 | 190 | OPTIONS rtsp://184.72.239.149:554/vod/mp4:BigBuckBunny_115k.mov RTSP/1.0\r\n 191 | CSeq: 2\r\n 192 | Session: 1234567890\r\n 193 | \r\n 194 | 195 | ## SETUP 196 | 197 | The `SETUP` command looks like the following. Bear in mind that we must issue a command **per media track** for setting up (i.e. one for video and one for audio). 198 | 199 | SETUP rtsp://184.72.239.149:554/vod/mp4:BigBuckBunny_115k.mov/trackID=1 RTSP/1.0\r\n 200 | CSeq: 3\r\n 201 | Session: 1234567890\r\n 202 | Transport: RTP/AVP;unicast;client_port=9000-9001\r\n 203 | \r\n 204 | SETUP rtsp://184.72.239.149:554/vod/mp4:BigBuckBunny_115k.mov/trackID=2 RTSP/1.0\r\n 205 | CSeq: 4\r\n 206 | Session: 1234567890\r\n 207 | Transport: RTP/AVP;unicast;client_port=9000-9001\r\n 208 | \r\n 209 | 210 | The `Transport` field is important, as it tells the server what kind of protocol the server should be streaming with (RTP of course), as well as where we will want to receive incoming media data, specified in this part of the string (recall the Port 9000 and 9001 that was mentioned earlier). We usually set the mode to `unicast`, signifying that the stream will only be broadcast to **one device**. Multicast options are possible, but the server must support it. What this means is that with one single audio/video stream, it can be replicated to many more devices than just a single one. This is not supported by most RTSP servers, and so `unicast` is always the safest mode to use. 211 | 212 | ## PLAY 213 | 214 | The `PLAY` command looks like this, once we have finished `SETUP`: 215 | 216 | PLAY rtsp://184.72.239.149:554/vod/mp4:BigBuckBunny_115k.mov RTSP/1.0\r\n 217 | CSeq: 5\r\n 218 | Session: 1234567890\r\n 219 | \r\n 220 | 221 | Starting to look the same isn't it? As you can see so far, most of the commands follow the same structure. The main difference is the command issued at the beginning of the first string. 222 | 223 | ## PAUSE 224 | 225 | The `PAUSE` command looks like this, assuming that `PLAY` was issued before: 226 | 227 | PAUSE rtsp://184.72.239.149:554/vod/mp4:BigBuckBunny_115k.mov RTSP/1.0\r\n 228 | CSeq: 6\r\n 229 | Session: 1234567890\r\n 230 | \r\n 231 | 232 | ## TEARDOWN 233 | 234 | A `TEARDOWN` command looks like this, assuming that we are either in `PLAY` or `PAUSE`: 235 | 236 | TEARDOWN rtsp://184.72.239.149:554/vod/mp4:BigBuckBunny_115k.mov RTSP/1.0\r\n 237 | CSeq: 7\r\n 238 | Session: 1234567890\r\n 239 | \r\n 240 | 241 | Bear in mind that once you issue a `TEARDOWN` request, you need to reset the `CSeq` counter so that it starts at 1. 242 | 243 | # How to use the library 244 | 245 | `RTSPControl` already handles all of the intricacies that you have seen earlier. This is encapsulated by simple method calls. You would first create a `RTSPControl` object using one of two constructors: 246 | 247 | 1. `RTSPControl rtspControl = new RTSPControl(rtspURL);` 248 | 2. `RTSPControl rtspControl = new RTSPControl(rtspHost, rtspPort, videoFile);` 249 | 250 | In the first method, `rtspURL` is a string that is in the format of: `rtsp://address.of.server:port/pathToFileToPlay`, like we talked about earlier. In this case, you would do: `rtspURL = rtsp://184.72.239.149:554/vod/mp4:BigBuckBunny_115k.mov`. In the second method, you can specify the IP/Hostname of the RTSP server, the RTSP port and the path to the video file manually. `rtspHost` and `videoFile` are strings, while `rtspPort` is an integer. In this case, you would do: `new RTSPControl("184.72.239.149", 554, "vod/mp4:BigBuckBunny_115k.mov);`. By specifying `-1` as the server port, this automatically defaults to `554`. 251 | 252 | Once you have this established, the Datagram Socket connection is all done underneath the hood. After the constructor, you can call your standard RTSP server commands: 253 | 254 | * `RTSPDescribe()`: Issues a `DESCRIBE` command to the server 255 | * `RTSPOptions()`: Issues an `OPTIONS` command to the server 256 | * `RTSPSetup()`: Issues `SETUP` commands to the server for as many tracks as there are available 257 | * `RTSPPlay()`: Issues a `PLAY` command to the server 258 | * `RTSPPause()`: Issues a `PAUSE` command to the server 259 | * `RTSPTeardown()`: Issues a `TEARDOWN` command to the server 260 | 261 | When `PLAY` is invoked, within the `RTSPControl` class, a `Timer` event gets initiated and every 20 milliseconds, media data packets are read from port 9000. These data packets are simply stored into a `Byte` buffer and displayed on the screen in hexidecimal format. In terms of using the actual data itself, this has not been implemented as different media protocols structure their media packets differently. It will actually be up to you to parse through the data yourself and extract the meaningful data. 262 | 263 | Also, as an additional feature, we keep the connection to the RTSP server alive by periodically sending an innocuous `OPTIONS` request. This way if you try to send a command to the server, this avoids any connection timeouts. 264 | 265 | To compile the library, simply do: 266 | 267 | javac RTSPControl.java 268 | 269 | # How to use the test GUI 270 | 271 | All you have to do is compile the source, then run the code: 272 | 273 | javac RTSPTest.java 274 | java RTSPTest 275 | 276 | Once you run it, you will be provided with a simple interface where you can enter in a RTSP URL, as well as various buttons to select that issues the aforementioned RTSP commands. Simply follow the workflow that has been laid out previously. When you run the RTSP test GUI, the text field will already be populated with the RTSP URL to the test Wowza server. In order to create a connection to the server, you need to push ENTER when the text field has focus, **even with the default text in the field**. When you eventually get to the `PLAY` command, you will see the bytes written to the screen as well as how many bytes were written at each reading of the port where the media data packets are coming in. 277 | 278 | # References 279 | 280 | This link was an invaluable resource: http://folk.uio.no/meccano/reflector/smallclient.html. This code was also based on a very popular RTP/RTSP streaming assignment which can be found here: http://www.csee.umbc.edu/~pmundur/courses/CMSC691C/lab5-kurose-ross.html. The full solution to this assignment can be found here: https://github.com/sjbarag/ECE-C433/tree/master/proj3. 281 | 282 | # Permissions 283 | 284 | This code is protected by the MIT License. As such, you may use this code in any products you are developing, whether it be open or closed source to your heart's content. A copy of the license is included with this repo. The only thing I request is that you acknowledge me if you decide to use this library for your application. Thanks, and have fun! 285 | -------------------------------------------------------------------------------- /RTSPControl.java: -------------------------------------------------------------------------------- 1 | import java.io.BufferedReader; 2 | import java.io.BufferedWriter; 3 | import java.io.IOException; 4 | import java.io.InputStreamReader; 5 | import java.io.OutputStreamWriter; 6 | import java.net.DatagramPacket; 7 | import java.net.DatagramSocket; 8 | import java.net.InetAddress; 9 | import java.net.Socket; 10 | import java.net.SocketException; 11 | import java.net.UnknownHostException; 12 | import java.util.StringTokenizer; 13 | import java.util.Timer; 14 | import java.util.TimerTask; 15 | 16 | public class RTSPControl { 17 | // RTP Variables 18 | 19 | // UDP packet received from server 20 | private DatagramPacket rcvdp = null; 21 | // Socket used to send and receive UDP packets 22 | private DatagramSocket RTPsocket = null; 23 | // Port where client will receive the RTP packets 24 | private int RTP_RCV_PORT = 9000; 25 | 26 | // Timer used to receive data from the UDP socket 27 | Timer timer = null; 28 | Timer timerOptions = null; 29 | 30 | // Buffer to receive the data 31 | byte[] buf = null; 32 | 33 | // RTSP Variables 34 | // State variables 35 | enum RTSPState { 36 | INIT, 37 | READY, 38 | PLAYING 39 | }; 40 | 41 | // Request variables 42 | enum RTSPRequest { 43 | SETUP, 44 | DESCRIBE, 45 | PLAY, 46 | PAUSE, 47 | TEARDOWN, 48 | OPTIONS 49 | }; 50 | 51 | // Current state of the client 52 | private RTSPState state = null; 53 | 54 | // Socket used to send/receive RTSP messages 55 | private Socket RTSPSocket = null; 56 | 57 | // Input and Output stream filters 58 | // Used to write RTSP messages and send to server 59 | private BufferedReader RTSPBufferedReader = null; 60 | 61 | // Used to receive RTSP messages and data 62 | private BufferedWriter RTSPBufferedWriter = null; 63 | 64 | // Video file / Location to play 65 | private String videoFile = null; 66 | 67 | // Sequence number of RTSP messages within the session 68 | // Initially set to zero 69 | private int RTSPSeqNb = 0; 70 | 71 | // Changed this to a string instead of an int 72 | // as the actual RTSP server I'm testing on issues sessions 73 | // in alphanumeric characters. Leave this as a string to be safe 74 | private String RTSPSessionID = null; // ID of RTSP sessions - given by RTSP server 75 | 76 | // Carriage return and new line to send to server 77 | private final String CRLF = "\r\n"; 78 | 79 | // Store server port to communicate with server 80 | // Usually 554 for RTSP 81 | private int serverPort = 554; 82 | 83 | // Flag to establish that we have set up our parameters 84 | private boolean isSetup = false; 85 | 86 | // Store host name 87 | private String hostName = null; 88 | 89 | // For the audio and video track IDs issued from 90 | // the server 91 | private int audioTrackID = -1; 92 | private int videoTrackID = -1; 93 | 94 | // Store the audio and video payload types 95 | // We will need this to figure out what data is being sent 96 | // from the server 97 | private int audioPT = -1; 98 | private int videoPT = -1; 99 | 100 | // Flags that tell us whether SETUP using video and audio are finished 101 | private boolean videoSetupDone = false; 102 | private boolean audioSetupDone = false; 103 | 104 | // The type of streaming protocol extracted from the 105 | // server 106 | private String streamingProtocolVideo = null; 107 | private String streamingProtocolAudio = null; 108 | 109 | // The total number of tracks 110 | private int numberOfTracks = 0; 111 | 112 | // Flag to determine if the trackID string is just 113 | // trackID or track 114 | private boolean galbarmFlag = false; 115 | 116 | // Boolean flag that tells us whether or not the ID of the track 117 | // is referenced by the key "trackID=" or "stream=" 118 | // GStreamer has it as the latter, while conventional RTSP servers 119 | // have it as the former 120 | boolean trackIDOrStream = true; 121 | 122 | // Variables that define the periodic options request sent to the 123 | // RTSP server 124 | // In order to prevent a time out, it's good to periodically send 125 | // something to the server to let you know that you're still here 126 | // You can do this very innocuously with an OPTIONS request 127 | // As such, every 45 seconds we send an OPTIONS request to let 128 | // the server know we're still here 129 | // We will also start the timer at about 15 seconds after we schedule 130 | // the task to ensure no conflicts 131 | private final int TIMEROPTIONSDELAY = 15000; 132 | private final int TIMEROPTIONSFREQUENCY = 45000; 133 | 134 | // Constructor 135 | // serverHost - Alphabetical URL or IP Address 136 | // etc. www.bublcam.com or 192.168.0.100 137 | // fileName - Exact relevant path and filename to what we want to play 138 | // Set serverPort to -1 for default port of 554 139 | public RTSPControl(String serverHost, int serverPort, String fileName) { 140 | if (fileName == null) 141 | throw new IllegalArgumentException("RTSPTest: Filename must not be null"); 142 | 143 | if (serverHost == null) 144 | throw new IllegalArgumentException("RTSPTest: Server host must not be null"); 145 | 146 | if (serverPort < -1 || serverPort > 65535) 147 | throw new IllegalArgumentException("RTSPTest: Port must be between 0 and 65535"); 148 | 149 | // Figure out the server port 150 | if (serverPort == -1) // default port 151 | this.serverPort = 554; 152 | else 153 | this.serverPort = serverPort; 154 | 155 | // Store host name 156 | hostName = new String(serverHost); 157 | // Store file name 158 | videoFile = new String(fileName); 159 | 160 | // Set to false then set up 161 | isSetup = false; 162 | setUpConnectionAndParameters(); 163 | } 164 | 165 | // Constructor if given string URL 166 | public RTSPControl(String rtspURL) { 167 | 168 | // Parse out just the URL by itself (and port if applicable) 169 | String temp = "rtsp://"; 170 | int locOfRtsp = rtspURL.indexOf(temp); 171 | 172 | // Throw exception if rtsp:// is not accompanied with the URL 173 | if (locOfRtsp == -1) 174 | throw new IllegalArgumentException("Must give URL that begins with rtsp://"); 175 | 176 | // Obtain a string excluding the "rtsp://" bit 177 | String parsedURL = rtspURL.substring(locOfRtsp + temp.length()); 178 | 179 | // Extract the IP with the port address if possible 180 | // You also need to make sure that there is a **video file** we need to play 181 | // This means that there should be a slash that follows it. 182 | int indexOfSlash = parsedURL.indexOf("/"); 183 | String hostnameTemp; 184 | if (indexOfSlash != -1) 185 | hostnameTemp = parsedURL.substring(0, indexOfSlash); 186 | else 187 | throw new IllegalArgumentException("RTSP URL must end with a slash (/)"); 188 | 189 | // Check to see if there is a port specified. If there isn't, 190 | // assume default part 191 | int indexOfColon = hostnameTemp.indexOf(':'); 192 | if (indexOfColon != -1) { 193 | // Get the IP / DNS without the ':' 194 | this.hostName = hostnameTemp.substring(0, indexOfColon); 195 | 196 | // Get the port number that is after the colon 197 | try { 198 | this.serverPort = Integer.parseInt(hostnameTemp.substring(hostnameTemp.indexOf(':') + 1)); 199 | } catch (NumberFormatException nfe) { 200 | throw new IllegalArgumentException("Error: Port number is not a number"); 201 | } 202 | } 203 | else { 204 | this.hostName = hostnameTemp; 205 | this.serverPort = 554; 206 | } 207 | 208 | // Get the video file name now 209 | // If none is provided, this is null 210 | if (indexOfSlash + 1 > parsedURL.length()) 211 | videoFile = null; 212 | else 213 | videoFile = parsedURL.substring(indexOfSlash + 1); 214 | 215 | isSetup = false; 216 | setUpConnectionAndParameters(); 217 | } 218 | 219 | public String getRTSPURL() { 220 | return new String(hostName); 221 | } 222 | 223 | public int getServerPort() { 224 | return this.serverPort; 225 | } 226 | 227 | public String getVideoFilename() { 228 | if (this.videoFile != null) 229 | return new String(videoFile); 230 | else 231 | return null; 232 | } 233 | 234 | 235 | // Initialize TCP connection with server to exchange RTSP messages 236 | private void setUpConnectionAndParameters() { 237 | try { 238 | //System.out.println("Establishing TCP Connection to: " + hostName + " at Port: " + serverPort); 239 | InetAddress ServerIPAddr = InetAddress.getByName(hostName); 240 | RTSPSocket = new Socket(ServerIPAddr, serverPort); 241 | } catch (UnknownHostException e) { 242 | System.out.println("Could not find host"); 243 | e.printStackTrace(); 244 | } catch (IOException e) { 245 | System.out.println("Could not establish socket"); 246 | e.printStackTrace(); 247 | } 248 | 249 | // Set up buffers to read from and write to server 250 | try { 251 | //System.out.println("Set up read buffer"); 252 | RTSPBufferedReader = new BufferedReader(new InputStreamReader(RTSPSocket.getInputStream())); 253 | } catch (IOException e) { 254 | System.out.println("Could not create buffer to read from server"); 255 | e.printStackTrace(); 256 | } 257 | 258 | try { 259 | //System.out.println("Set up write buffer"); 260 | RTSPBufferedWriter = new BufferedWriter(new OutputStreamWriter(RTSPSocket.getOutputStream())); 261 | } catch (IOException e) { 262 | System.out.println("Could not create buffer to write to server"); 263 | e.printStackTrace(); 264 | } 265 | 266 | // Initialize state 267 | state = RTSPState.INIT; 268 | 269 | // Initialize buffer to capture enough bytes from the server 270 | buf = new byte[300000]; 271 | 272 | // Initialize parameters 273 | audioTrackID = -1; 274 | videoTrackID = -1; 275 | audioPT = -1; 276 | videoPT = -1; 277 | RTSPSessionID = null; 278 | numberOfTracks = 0; 279 | isSetup = true; 280 | RTPsocket = null; 281 | videoSetupDone = false; 282 | videoSetupDone = false; 283 | 284 | timerOptions = new Timer(); 285 | // Start an Options timer that incrementally sends a request every so often 286 | // This prevents the server from disconnecting 287 | // Delay by two seconds to ensure no conflicts in re-establishing connection 288 | timerOptions.scheduleAtFixedRate(new RTSPOptionsTimerTask(), TIMEROPTIONSDELAY, 289 | TIMEROPTIONSFREQUENCY); 290 | } 291 | 292 | // Integer code - 0 for no more setup calls required 293 | // >= 1 - An additional setup is required 294 | // -1 - Invalid server response 295 | public int RTSPSetup() { 296 | if (state != RTSPState.INIT) { 297 | System.out.println("Client is already set up or is playing content"); 298 | return 0; // We have already set up or are playing 299 | } 300 | 301 | if (numberOfTracks == 0) { 302 | System.out.println("No tracks to set up!"); 303 | return 0; 304 | } 305 | 306 | // Cancel the options timer request while we do this 307 | if (timerOptions != null) 308 | timerOptions.cancel(); 309 | 310 | // Increment RTSP sequence number 311 | RTSPSeqNb++; 312 | 313 | // Send SETUP message to server 314 | sendRTSPRequest(RTSPRequest.SETUP); 315 | 316 | // Wait for response code from server 317 | if (parseServerResponse() != 200) { 318 | System.out.println("Invalid Server Response"); 319 | return -1; 320 | } 321 | else { 322 | // Check to see what track IDs we have 323 | // If we have both, then we need to wait for another 324 | // setup call 325 | // We aren't ready until we set up all of our 326 | // tracks 327 | if (numberOfTracks == 0) { 328 | // Change current RTSP state to READY 329 | System.out.println("Client is ready"); 330 | state = RTSPState.READY; 331 | 332 | // Also establish our socket connection to the server 333 | if (RTPsocket == null) { 334 | try { 335 | RTPsocket = new DatagramSocket(RTP_RCV_PORT); 336 | 337 | // Set timeout value to 100 msec 338 | // Disable for now 339 | //RTPsocket.setSoTimeout(100); 340 | } 341 | 342 | // Catch exception here 343 | catch (SocketException se) { 344 | System.out.println("Socket Exception: " + se); 345 | System.exit(0); 346 | } 347 | } 348 | } 349 | 350 | // Restart Options timer 351 | timerOptions = new Timer(); 352 | timerOptions.scheduleAtFixedRate(new RTSPOptionsTimerTask(), TIMEROPTIONSDELAY, 353 | TIMEROPTIONSFREQUENCY); 354 | return numberOfTracks; 355 | } 356 | } 357 | 358 | public void RTSPPlay() { 359 | if (state != RTSPState.READY) { 360 | System.out.println("Client has not sent Setup Request yet"); 361 | return; 362 | } 363 | 364 | // Cancel Options timer while we pull the response from the server 365 | if (timerOptions != null) 366 | timerOptions.cancel(); 367 | 368 | // Increase the RTSP sequence number - This is the 369 | // next command to issue 370 | RTSPSeqNb++; 371 | 372 | // Send PLAY message to server 373 | sendRTSPRequest(RTSPRequest.PLAY); 374 | 375 | // Wait for response 376 | if (parseServerResponse() != 200) 377 | System.out.println("Invalid Server Response"); 378 | else { 379 | System.out.println("Starting playback - Starting Timer event"); 380 | // Set to play state 381 | state = RTSPState.PLAYING; 382 | 383 | if (RTPsocket != null) { 384 | try { 385 | if (RTPsocket.isClosed()) 386 | RTPsocket = new DatagramSocket(RTP_RCV_PORT); 387 | } catch (SocketException e) { 388 | System.out.println("Could not reconnect to socket"); 389 | e.printStackTrace(); 390 | } 391 | } 392 | 393 | // Initialize timer to receive events from server 394 | timer = new Timer(); 395 | // Start it now, with 20 millisecond intervals 396 | timer.scheduleAtFixedRate(new RTSPTimerTask(), 0, 20); 397 | } 398 | 399 | // Restart Options timer event 400 | // Even if we are playing content, we still need to keep our 401 | // connection alive 402 | timerOptions = new Timer(); 403 | timerOptions.scheduleAtFixedRate(new RTSPOptionsTimerTask(), TIMEROPTIONSDELAY, 404 | TIMEROPTIONSFREQUENCY); 405 | } 406 | 407 | public void RTSPPause() { 408 | if (state != RTSPState.PLAYING) { 409 | System.out.println("Client is not playing content right now"); 410 | return; 411 | } 412 | 413 | // Increase the RTSP sequence number 414 | RTSPSeqNb++; 415 | 416 | // Send PAUSE message to server 417 | sendRTSPRequest(RTSPRequest.PAUSE); 418 | 419 | // Wait for response 420 | if (parseServerResponse() != 200) 421 | System.out.println("Invalid Server Response"); 422 | else { 423 | state = RTSPState.READY; 424 | System.out.println("Pausing playback - Cancelling Timer event"); 425 | timer.cancel(); 426 | } 427 | 428 | // Restart Options timer while we are paused 429 | // Notice that we don't cancel this timer as this was already done in 430 | // PLAY 431 | timerOptions = new Timer(); 432 | timerOptions.scheduleAtFixedRate(new RTSPOptionsTimerTask(), TIMEROPTIONSDELAY, 433 | TIMEROPTIONSFREQUENCY); 434 | } 435 | 436 | public void RTSPTeardown() { 437 | // You can only call TEARDOWN after the connections have been set up, 438 | // or if you are playing content 439 | if (state == RTSPState.INIT) { 440 | System.out.println("Client is in initialize stage - No need to teardown"); 441 | return; 442 | } 443 | 444 | // Increase RTSP Sequence number 445 | RTSPSeqNb++; 446 | 447 | // Send TEARDOWN message to the server 448 | sendRTSPRequest(RTSPRequest.TEARDOWN); 449 | 450 | // Wait for server response 451 | if (parseServerResponse() != 200) 452 | System.out.println("Invalid Server Response"); 453 | else { 454 | System.out.println("Teardown - Changing Client state back to INIT"); 455 | 456 | // Reset all parameters 457 | resetParameters(); 458 | } 459 | } 460 | 461 | public void resetParameters() { 462 | state = RTSPState.INIT; 463 | 464 | // Reset sequence number 465 | RTSPSeqNb = 0; 466 | 467 | // Reset all other parameters 468 | audioTrackID = -1; 469 | videoTrackID = -1; 470 | audioPT = -1; 471 | videoPT = -1; 472 | RTSPSessionID = null; 473 | numberOfTracks = 0; 474 | isSetup = false; 475 | buf = null; 476 | videoSetupDone = false; 477 | audioSetupDone = false; 478 | 479 | // Cancel all timing events if Teardown option is executed 480 | if (timer != null) { 481 | timer.cancel(); 482 | timer = null; 483 | } 484 | 485 | if (timerOptions != null) { 486 | timerOptions.cancel(); 487 | timerOptions = null; 488 | } 489 | 490 | // Close connection as well 491 | if (RTPsocket != null && !RTPsocket.isClosed()) { 492 | RTPsocket.close(); 493 | RTPsocket = null; 494 | } 495 | } 496 | 497 | // Send a request to see what the options are 498 | public void RTSPOptions() { 499 | // Remove because we would like to send an OPTIONS request no matter what 500 | // state we are in 501 | //if (state != RTSPState.INIT) { 502 | // System.out.println("Must be in INIT stage before requesting Options"); 503 | // return; 504 | //} 505 | 506 | if (!isSetup) 507 | setUpConnectionAndParameters(); 508 | 509 | // Increase RTSP Sequence Number 510 | RTSPSeqNb++; 511 | 512 | // Send OPTIONS message to the server 513 | sendRTSPRequest(RTSPRequest.OPTIONS); 514 | 515 | // Wait for server response 516 | if (parseServerResponse() != 200) 517 | System.out.println("Invalid Server Response"); 518 | else { 519 | System.out.println("Options Request succeeded"); 520 | // We don't need to change the state 521 | } 522 | } 523 | 524 | // Send a request for DESCRIBING what is available 525 | public void RTSPDescribe() { 526 | if (state != RTSPState.INIT) { 527 | System.out.println("Must be in INIT stage before requesting DESCRIBE"); 528 | return; 529 | } 530 | 531 | if (!isSetup) 532 | setUpConnectionAndParameters(); 533 | 534 | // Cancel Options timer while we pull this information 535 | if (timerOptions != null) 536 | timerOptions.cancel(); 537 | 538 | // Increase RTSP Sequence Number 539 | RTSPSeqNb++; 540 | 541 | // Send OPTIONS message to the server 542 | sendRTSPRequest(RTSPRequest.DESCRIBE); 543 | 544 | // Wait for server response 545 | if (parseServerResponse() != 200) 546 | System.out.println("Invalid Server Response"); 547 | else { 548 | System.out.println("Describe Request succeeded"); 549 | // We don't need to change the state 550 | } 551 | 552 | // Restart Options timer 553 | timerOptions = new Timer(); 554 | timerOptions.scheduleAtFixedRate(new RTSPOptionsTimerTask(), TIMEROPTIONSDELAY, 555 | TIMEROPTIONSFREQUENCY); 556 | } 557 | 558 | // Goes through the received string from the server 559 | // Also allows us to parse through and see if there is track 560 | // information and the type of transport this server supports 561 | public int parseServerResponse() { 562 | int replyCode = 0; 563 | 564 | try { 565 | // Read first line - Status line. If all goes well 566 | // this should give us: RTSP/1.0 200 OK 567 | String statusLine = RTSPBufferedReader.readLine(); 568 | 569 | if (statusLine == null) { 570 | System.out.println("Could not communiate with server"); 571 | return -1; 572 | } 573 | 574 | System.out.println(statusLine); 575 | 576 | // Tokenize the string and grab the next token, which is 577 | // the status code 578 | StringTokenizer tokens = new StringTokenizer(statusLine, " \t\n\r\f"); 579 | tokens.nextToken(); // Gives us RTSP/1.0 580 | // Give us reply code 581 | replyCode = Integer.parseInt(tokens.nextToken()); 582 | System.out.println("*** Reply Code: " + replyCode); 583 | // If the reply code is 200, then we are solid 584 | if (replyCode == 200) { // begin if 585 | // Cycle through the rest of the lines, delimited by \n 586 | // and print to the screen. Also grab the relevant information 587 | // we need 588 | //for (String line = RTSPBufferedReader.readLine(); line != null; 589 | // line = RTSPBufferedReader.readLine()) { // begin for 590 | 591 | // NEW: First we need to wait to see if we are ready to read 592 | String line; 593 | try { 594 | while (!RTSPBufferedReader.ready()) 595 | continue; 596 | } 597 | catch(IOException e) { 598 | System.out.println("Could not read from read buffer"); 599 | //e.printStackTrace(); 600 | return -1; 601 | } 602 | 603 | // If we are, while there is still data in the buffer... 604 | while (RTSPBufferedReader.ready()) { // begin for 605 | line = RTSPBufferedReader.readLine(); 606 | if (line == null) // Also check to see if we have reached the end 607 | break; // of the stream 608 | 609 | System.out.println(line); 610 | // Tokenize 611 | // Also includes semi-colons 612 | tokens = new StringTokenizer(line, " \t\n\r\f;:"); 613 | 614 | // Now go through each token 615 | while (tokens.hasMoreTokens()) { // begin while1 616 | // Grab token 617 | String part = tokens.nextToken(); 618 | 619 | // When we are using DESCRIBE - We are looking for the 620 | // track numbers - This will allow us to request 621 | // that particular stream of data so we can pipe it 622 | // into our decoder buffers 623 | 624 | // Usually looks like this: 625 | // m=audio num RTP/AVP PT 626 | // or 627 | // m=video num RTP/AVP PT 628 | // PT is the payload type or the type of media that is represented 629 | // for the audio or video (i.e. audio: MP4, MP3, etc., video: 630 | // H264, MPEG1, etc.) 631 | if (part.equals("m=audio") && audioTrackID == -1) { // begin if2 632 | // Skip next token 633 | tokens.nextToken(); 634 | 635 | // Obtain the streaming protocol for the Audio 636 | // Usually RTP/AVP 637 | streamingProtocolAudio = new String(tokens.nextToken()); 638 | 639 | // Next token should contain our payload type 640 | audioPT = Integer.parseInt(tokens.nextToken()); 641 | System.out.println("*** Audio PT: " + audioPT); 642 | 643 | // Now advance each line until we hit "a=control" 644 | while (true) { // begin while2 645 | line = RTSPBufferedReader.readLine(); 646 | System.out.println(line); 647 | if (line.indexOf("a=control") != -1 || line == null) 648 | break; 649 | } // end while2 650 | 651 | if (line == null) { // begin if3 652 | System.out.println("Could not find a=control String"); 653 | return replyCode; 654 | } // end if3 655 | 656 | // Once we hit "a=control", get the track number 657 | StringTokenizer controlTokens = new StringTokenizer(line, " \t\n\r\f;:"); 658 | // Skip over a=control 659 | controlTokens.nextToken(); 660 | // This should now contain our trackID 661 | String trackID = controlTokens.nextToken(); 662 | 663 | // Look for the key trackID or stream and adjust accordingly 664 | if (trackID.indexOf("trackID") != -1) { 665 | this.audioTrackID = Integer.parseInt(trackID.substring(8, 9)); 666 | this.trackIDOrStream = true; 667 | this.galbarmFlag = false; 668 | } 669 | ///// Fix thanks to galbarm 670 | else if (trackID.indexOf("track") != -1) { 671 | this.audioTrackID = Integer.parseInt(trackID.substring((trackID.indexOf("track")+5),trackID.length()));// 5, here is length of track i.e, "track".length() 672 | this.trackIDOrStream = true; 673 | this.galbarmFlag = true; 674 | } 675 | else if (trackID.indexOf("stream") != -1) { 676 | this.audioTrackID = Integer.parseInt(trackID.substring(7, 8)); 677 | this.trackIDOrStream = false; 678 | } 679 | 680 | System.out.println("*** Audio Track: " + audioTrackID); 681 | numberOfTracks++; 682 | 683 | // Break out of this loop and continue reading the other lines 684 | break; 685 | } // end if2 686 | else if (part.equals("m=video") && videoTrackID == -1) { // begin if2 687 | // Skip next token 688 | tokens.nextToken(); 689 | 690 | // Obtain the streaming protocol for the Audio 691 | // Usually RTP/AVP 692 | streamingProtocolVideo = new String(tokens.nextToken()); 693 | 694 | // Next token should contain our payload type 695 | videoPT = Integer.parseInt(tokens.nextToken()); 696 | System.out.println("*** Video PT: " + videoPT); 697 | 698 | // Now advance each line until we hit "a=control" 699 | while (true) { // begin while2 700 | line = RTSPBufferedReader.readLine(); 701 | System.out.println(line); 702 | if (line.indexOf("a=control") != -1 || line == null) 703 | break; 704 | } // end while2 705 | 706 | if (line == null) { // begin if3 707 | System.out.println("Could not find a=control String"); 708 | return replyCode; 709 | } // end if3 710 | 711 | // Once we hit "a=control", get the track number 712 | StringTokenizer controlTokens = new StringTokenizer(line, " \t\n\r\f;:"); 713 | // Skip over a=control 714 | controlTokens.nextToken(); 715 | // This should now contain our trackID 716 | String trackID = controlTokens.nextToken(); 717 | 718 | // Look for the key trackID or stream and adjust accordingly 719 | if (trackID.indexOf("trackID") != -1) { 720 | this.videoTrackID = Integer.parseInt(trackID.substring(8, 9)); 721 | this.galbarmFlag = false; 722 | this.trackIDOrStream = true; 723 | } 724 | ///// Fix thanks to galbarm 725 | else if (trackID.indexOf("track") != -1) { 726 | this.videoTrackID = Integer.parseInt(trackID.substring((trackID.indexOf("track")+5),trackID.length()));// 5, here is length of track i.e, "track".length() 727 | this.galbarmFlag = true; 728 | this.trackIDOrStream = true; 729 | } 730 | 731 | else if (trackID.indexOf("stream") != -1) { 732 | this.videoTrackID = Integer.parseInt(trackID.substring(7, 8)); 733 | this.trackIDOrStream = false; 734 | } 735 | 736 | System.out.println("*** Video Track: " + videoTrackID); 737 | numberOfTracks++; 738 | 739 | // Break out of this loop and continue reading other lines 740 | break; 741 | } // end if2 742 | 743 | // Extract the Session ID for subsequent setups 744 | else if (part.equals("Session") && RTSPSessionID == null) { // begin if2 745 | this.RTSPSessionID = tokens.nextToken(); 746 | System.out.println("*** Session ID: " + RTSPSessionID); 747 | // Break out of this loop and continue reading other lines 748 | break; 749 | } // end if2 750 | } // end while1 751 | } // end for 752 | } // end if 753 | } catch (IOException e) { 754 | System.out.println("Could not read in string from buffer"); 755 | e.printStackTrace(); 756 | } 757 | return replyCode; 758 | } 759 | 760 | public void sendRTSPRequest(RTSPRequest request) { 761 | // Set up base String 762 | String requestType; 763 | StringBuilder stringToSend = new StringBuilder(); 764 | switch (request) { 765 | case SETUP: 766 | requestType = new String("SETUP"); 767 | break; 768 | case DESCRIBE: 769 | requestType = new String("DESCRIBE"); 770 | break; 771 | case PLAY: 772 | requestType = new String("PLAY"); 773 | break; 774 | case TEARDOWN: 775 | requestType = new String("TEARDOWN"); 776 | break; 777 | case PAUSE: 778 | requestType = new String("PAUSE"); 779 | break; 780 | case OPTIONS: 781 | requestType = new String("OPTIONS"); 782 | break; 783 | default: 784 | throw new IllegalArgumentException("Invalid request type"); 785 | } 786 | 787 | // This handles actual strings we are going to send to the server 788 | switch(request) { 789 | case SETUP: 790 | if (videoSetupDone && audioSetupDone) 791 | System.out.println("Setup already established"); 792 | else { 793 | try { 794 | // Set up video track if we haven't done it yet 795 | if (videoTrackID != -1 && !videoSetupDone) { 796 | System.out.println("*** Setting up Video Track: "); 797 | if (this.trackIDOrStream) { 798 | if (videoFile != null) { 799 | //////// Fix thanks to galbarm 800 | if (this.galbarmFlag) 801 | stringToSend.append(requestType + " rtsp://" + hostName + ":" + serverPort + 802 | "/" + videoFile + "/track=" + videoTrackID + " RTSP/1.0" + CRLF); 803 | else 804 | stringToSend.append(requestType + " rtsp://" + hostName + ":" + serverPort + 805 | "/" + videoFile + "/trackID=" + videoTrackID + " RTSP/1.0" + CRLF); 806 | } 807 | else { 808 | if (this.galbarmFlag) 809 | stringToSend.append(requestType + " rtsp://" + hostName + ":" + serverPort + 810 | "/track=" + videoTrackID + " RTSP/1.0" + CRLF); 811 | else 812 | stringToSend.append(requestType + " rtsp://" + hostName + ":" + serverPort + 813 | "/trackID=" + videoTrackID + " RTSP/1.0" + CRLF); 814 | } 815 | } 816 | else { 817 | if (videoFile != null) 818 | stringToSend.append(requestType + " rtsp://" + hostName + ":" + serverPort + 819 | "/" + videoFile + "/stream=" + videoTrackID + " RTSP/1.0" + CRLF); 820 | else 821 | stringToSend.append(requestType + " rtsp://" + hostName + ":" + serverPort + 822 | "/stream=" + videoTrackID + " RTSP/1.0" + CRLF); 823 | } 824 | 825 | stringToSend.append("CSeq: " + RTSPSeqNb + CRLF); 826 | 827 | if (RTSPSessionID != null) 828 | stringToSend.append("Session: " + RTSPSessionID + CRLF); 829 | 830 | stringToSend.append("Transport: " + streamingProtocolVideo + ";unicast;client_port=" + 831 | RTP_RCV_PORT + "-" + (RTP_RCV_PORT+1) + CRLF + CRLF); 832 | RTSPBufferedWriter.write(stringToSend.toString()); 833 | System.out.println(stringToSend.toString()); 834 | RTSPBufferedWriter.flush(); 835 | numberOfTracks--; 836 | videoSetupDone = true; 837 | } 838 | 839 | // After, set up the audio track 840 | else if (audioTrackID != -1 && !audioSetupDone) { 841 | System.out.println("*** Setting up Audio Track: "); 842 | if (this.trackIDOrStream) { 843 | if (videoFile != null) { 844 | if (this.galbarmFlag) 845 | stringToSend.append(requestType + " rtsp://" + hostName + ":" + serverPort + 846 | "/" + videoFile + "/track=" + audioTrackID + " RTSP/1.0" + CRLF); 847 | else 848 | stringToSend.append(requestType + " rtsp://" + hostName + ":" + serverPort + 849 | "/" + videoFile + "/trackID=" + audioTrackID + " RTSP/1.0" + CRLF); 850 | } 851 | else { 852 | if (this.galbarmFlag) 853 | stringToSend.append(requestType + " rtsp://" + hostName + ":" + serverPort + 854 | "/track=" + audioTrackID + " RTSP/1.0" + CRLF); 855 | else 856 | stringToSend.append(requestType + " rtsp://" + hostName + ":" + serverPort + 857 | "/trackID=" + audioTrackID + " RTSP/1.0" + CRLF); 858 | } 859 | } 860 | else { 861 | if (videoFile != null) 862 | stringToSend.append(requestType + " rtsp://" + hostName + ":" + serverPort + 863 | "/" + videoFile + "/stream=" + audioTrackID + " RTSP/1.0" + CRLF); 864 | else 865 | stringToSend.append(requestType + " rtsp://" + hostName + ":" + serverPort + 866 | "/stream=" + audioTrackID + " RTSP/1.0" + CRLF); 867 | 868 | } 869 | 870 | stringToSend.append("CSeq: " + RTSPSeqNb + CRLF); 871 | 872 | if (RTSPSessionID != null) 873 | stringToSend.append("Session: " + RTSPSessionID + CRLF); 874 | 875 | stringToSend.append("Transport: " + streamingProtocolAudio + ";unicast;client_port=" + 876 | RTP_RCV_PORT + "-" + (RTP_RCV_PORT+1) + CRLF + CRLF); 877 | RTSPBufferedWriter.write(stringToSend.toString()); 878 | System.out.println(stringToSend.toString()); 879 | RTSPBufferedWriter.flush(); 880 | numberOfTracks--; 881 | audioSetupDone = true; 882 | } 883 | } catch(IOException e) { 884 | System.out.println("Could not write to write buffer"); 885 | e.printStackTrace(); 886 | } 887 | } 888 | break; 889 | case DESCRIBE: // Make sure we call DESCRIBE first 890 | case PLAY: // Case when we wish to issue a PLAY request 891 | case PAUSE: // Same for PAUSE 892 | case TEARDOWN: // Also same for TEARDOWN 893 | case OPTIONS: // Also same for OPTIONS 894 | try { 895 | if (videoFile != null) 896 | stringToSend.append(requestType + " rtsp://" + hostName + ":" + serverPort + 897 | "/" + videoFile + " RTSP/1.0" + CRLF); 898 | else 899 | stringToSend.append(requestType + " rtsp://" + hostName + ":" + serverPort + 900 | "/ RTSP/1.0" + CRLF); 901 | 902 | // Send sequence number 903 | // If there is no session ID, this is the last thing we send 904 | if (RTSPSessionID == null) 905 | stringToSend.append("CSeq: " + RTSPSeqNb + CRLF + CRLF); 906 | // Send session number if applicable 907 | else { 908 | stringToSend.append("CSeq: " + RTSPSeqNb + CRLF); 909 | stringToSend.append("Session: " + RTSPSessionID + CRLF + CRLF); 910 | } 911 | RTSPBufferedWriter.write(stringToSend.toString()); 912 | System.out.println(stringToSend.toString()); 913 | RTSPBufferedWriter.flush(); 914 | } 915 | catch (IOException e) { 916 | System.out.println("Could not write to write buffer"); 917 | e.printStackTrace(); 918 | } 919 | break; 920 | default: 921 | throw new RuntimeException("Invalid Client State"); 922 | } 923 | } 924 | 925 | // This timer task gets invoked every so often to ensure that the connection 926 | // is still alive and doesn't time out 927 | private class RTSPOptionsTimerTask extends TimerTask { 928 | @Override 929 | public void run() { 930 | RTSPOptions(); 931 | } 932 | } 933 | 934 | // Task that gets invoked after every 20 msec 935 | private class RTSPTimerTask extends TimerTask { 936 | 937 | @Override 938 | // Every 20 seconds, read from the socket connection 939 | public void run() { 940 | // Construct a DatagramPacket 941 | rcvdp = new DatagramPacket(buf, buf.length); 942 | 943 | try { 944 | // Receive the data 945 | if (!RTPsocket.isClosed()) { 946 | RTPsocket.receive(rcvdp); 947 | 948 | byte[] data = rcvdp.getData(); 949 | int length = rcvdp.getLength(); 950 | System.out.println("Data Received: "); 951 | 952 | // Print out its length 953 | System.out.println("Length of data: " + length); 954 | 955 | // Print the data itself 956 | System.out.println("Data: "); 957 | String hexChars = bytesToHex(data, length); 958 | System.out.println(hexChars); 959 | } 960 | } catch (SocketException se) { // We need to catch here if we decide to invoke TEARDOWN 961 | // while we are waiting for data coming from the DatagramSocket 962 | System.out.println("Socket connection closed"); 963 | this.cancel(); 964 | } 965 | catch (IOException e) { 966 | System.out.println("Could not read from socket"); 967 | e.printStackTrace(); 968 | this.cancel(); 969 | return; 970 | } 971 | } 972 | } 973 | 974 | // Convenience method to convert a byte array into a string 975 | // Source: http://stackoverflow.com/a/9855338/3250829 976 | // Tests shown to be the fastest conversion routine available 977 | // beating all other built-in ones in the Java API 978 | 979 | // For our byte to hex conversion routine 980 | final char[] hexArray = "0123456789ABCDEF".toCharArray(); 981 | 982 | private String bytesToHex(byte[] bytes, int length) { 983 | char[] hexChars = new char[length * 2]; 984 | for ( int j = 0; j < length; j++ ) { 985 | int v = bytes[j] & 0xFF; 986 | hexChars[j * 2] = hexArray[v >>> 4]; 987 | hexChars[j * 2 + 1] = hexArray[v & 0x0F]; 988 | } 989 | return new String(hexChars); 990 | } 991 | 992 | //private String bytesToHex(byte[] bytes) { 993 | // return bytesToHex(bytes, bytes.length); 994 | //} 995 | } 996 | --------------------------------------------------------------------------------