├── .gitignore ├── LICENSE ├── ManheimNotes.txt ├── README.md ├── RTSP Exchange.txt ├── TODO.txt ├── pom.xml └── src ├── main ├── java │ └── xpertss │ │ ├── SourceException.java │ │ ├── media │ │ ├── MediaChannel.java │ │ ├── MediaConsumer.java │ │ └── MediaType.java │ │ ├── mime │ │ ├── Header.java │ │ ├── HeaderParser.java │ │ ├── HeaderTokenizer.java │ │ ├── HeaderValue.java │ │ ├── Headers.java │ │ ├── MalformedException.java │ │ ├── MimeFlavor.java │ │ ├── Parameter.java │ │ ├── impl │ │ │ ├── ComplexHeaderParser.java │ │ │ ├── ComplexValueHeader.java │ │ │ ├── HttpHeaderParserProvider.java │ │ │ ├── MailHeaderParserProvider.java │ │ │ ├── NamedHeaderValue.java │ │ │ ├── ParameterizedHeaderParser.java │ │ │ ├── QuotedParameter.java │ │ │ ├── RtspHeaderParserProvider.java │ │ │ ├── SimpleHeaderParser.java │ │ │ ├── SimpleHeaderValue.java │ │ │ ├── SimpleParameter.java │ │ │ └── SingleValueHeader.java │ │ └── spi │ │ │ └── HeaderParserProvider.java │ │ ├── net │ │ ├── OptionalSocketOptions.java │ │ ├── SSLSocketOptions.java │ │ ├── SocketOptions.java │ │ └── SslClientAuth.java │ │ ├── nio │ │ ├── AcceptHandler.java │ │ ├── Checkable.java │ │ ├── ConnectHandler.java │ │ ├── DataHandler.java │ │ ├── DeferredNioAction.java │ │ ├── NioAction.java │ │ ├── NioProvider.java │ │ ├── NioReactor.java │ │ ├── NioReader.java │ │ ├── NioService.java │ │ ├── NioSession.java │ │ ├── NioStats.java │ │ ├── NioWriter.java │ │ ├── ReadyState.java │ │ └── Selectable.java │ │ ├── rtsp │ │ ├── RtspClient.java │ │ ├── RtspException.java │ │ ├── RtspHandler.java │ │ ├── RtspMethod.java │ │ ├── RtspPlayer.java │ │ ├── RtspRequest.java │ │ ├── RtspResponse.java │ │ ├── RtspResponseHandler.java │ │ ├── RtspSession.java │ │ ├── RtspState.java │ │ └── RtspStatus.java │ │ └── utils │ │ ├── SafeProxy.java │ │ ├── UserAgent.java │ │ └── Utils.java └── resources │ ├── META-INF │ └── services │ │ └── xpertss.mime.spi.HeaderParserProvider │ └── version.properties └── test └── java └── xpertss ├── mime ├── HeaderParserTest.java └── HeadersTest.java ├── rtsp ├── RtspClientTest.java ├── RtspMethodTest.java └── RtspResponseTest.java └── utils ├── UserAgentTest.java └── UtilsTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /.idea/** 3 | /target/** 4 | *.iml 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | 341 | -------------------------------------------------------------------------------- /ManheimNotes.txt: -------------------------------------------------------------------------------- 1 | So I noticed that I am receiving a sender report for each stream roughly every 5 2 | seconds (give or take 10 ms). The sender report for video appears to be offset 3 | behind audio by roughly 1.5 seconds. 4 | 5 | 6 | It does not appear that Darwin honors the Connection: close header when passed 7 | from the client. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RtspClient 2 | A simple RTSP Client implementation using Java NIO 3 | 4 | The rtsp-client is a Java NIO implementation of a RTSP client. At present it only 5 | supports player clients but a future release will add publishing clients. The API 6 | is extensible so customization is possible. 7 | 8 | Example; 9 | 10 | ```` 11 | RtspClient client = new RtspClient(); 12 | RtspPlayer player = new RtspPlayer(client, new MediaConsumer() { 13 | 14 | private Map> channels = new HashMap<>(); 15 | 16 | @Override 17 | public MediaDescription[] select(SessionDescription sdp) 18 | { 19 | return sdp.getMediaDescriptions(); 20 | } 21 | 22 | @Override 23 | public void createChannel(MediaChannel channel) 24 | { 25 | final MediaType type = channel.getType(); 26 | Range range = channel.getChannels(); 27 | channels.put(range.getLower(), new Consumer() { 28 | long start = System.nanoTime(); 29 | @Override public void apply(ByteBuffer data) { 30 | long millis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start); 31 | int seq = (int) (data.getShort(2) & 0xffff); 32 | long ts = (long) (data.getInt(4) & 0xffffffff); 33 | System.out.println(String.format("%s: %010d {seq=%05d, ts=%d, size=%04d}", type.name(), millis, seq, ts, data.remaining())); 34 | } 35 | }); 36 | channels.put(range.getUpper(), new Consumer() { 37 | @Override 38 | public void apply(ByteBuffer byteBuffer) { 39 | System.out.println(String.format("%s - RTP Sender Report Received", type.name())); 40 | } 41 | }); 42 | } 43 | 44 | @Override 45 | public void destroyChannels() 46 | { 47 | channels.clear(); 48 | } 49 | 50 | @Override 51 | public void consume(int channelId, ByteBuffer data) 52 | { 53 | Consumer handler = channels.get(channelId); 54 | if(handler == null) System.out.println(String.format("Received packet on unknown channel %d", channelId)); 55 | else handler.apply(data); 56 | } 57 | 58 | @Override 59 | public void handle(Throwable t) 60 | { 61 | t.printStackTrace(); 62 | } 63 | 64 | 65 | }); 66 | 67 | player.setReadTimeout(5000); 68 | player.start(URI.create("rtsp://stream.manheim.com:999/AVAIL.sdp")); 69 | assertEquals(RtspState.Activating, player.getState()); 70 | client.await(3, TimeUnit.SECONDS); 71 | assertEquals(RtspState.Active, player.getState()); 72 | player.pause(); 73 | assertEquals(RtspState.Pausing, player.getState()); 74 | client.await(3, TimeUnit.SECONDS); 75 | assertEquals(RtspState.Paused, player.getState()); 76 | player.play(); 77 | assertEquals(RtspState.Activating, player.getState()); 78 | client.await(3, TimeUnit.SECONDS); 79 | assertEquals(RtspState.Active, player.getState()); 80 | player.stop(); 81 | assertEquals(RtspState.Stopping, player.getState()); 82 | client.await(); 83 | assertEquals(RtspState.Stopped, player.getState()); 84 | 85 | ```` 86 | -------------------------------------------------------------------------------- /RTSP Exchange.txt: -------------------------------------------------------------------------------- 1 | OPTIONS rtsp://10.103.32.109:5001/AAA01.sdp RTSP/1.0 2 | CSeq: 2 3 | User-Agent: MMP/2.0 (MSIE 8.0; Windows NT 6.1) 4 | 5 | RTSP/1.0 200 OK 6 | Server: DSS/6.0.3 (Build/526.3; Platform/Linux; Release/Darwin Streaming Server; State/Development; ) 7 | Cseq: 2 8 | Public: DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, OPTIONS, ANNOUNCE, RECORD 9 | 10 | 11 | DESCRIBE rtsp://10.103.32.109:5001/AAA01.sdp RTSP/1.0 12 | CSeq: 3 13 | User-Agent: MMP/2.0 (MSIE 8.0; Windows NT 6.1) 14 | Accept: application/sdp 15 | 16 | RTSP/1.0 200 OK 17 | Server: DSS/6.0.3 (Build/526.3; Platform/Linux; Release/Darwin Streaming Server; State/Development; ) 18 | Cseq: 3 19 | Cache-Control: no-cache 20 | Content-length: 667 21 | Date: Thu, 12 Apr 2012 14:38:22 GMT 22 | Expires: Thu, 12 Apr 2012 14:38:22 GMT 23 | Content-Type: application/sdp 24 | x-Accept-Retransmit: our-retransmit 25 | x-Accept-Dynamic-Rate: 1 26 | Content-Base: rtsp://10.103.32.109:5001/AAA01.sdp/ 27 | 28 | v=0 29 | o=RTSP 2285599416 34 IN IP4 172.16.10.1 30 | s=Lane 1 31 | c=IN IP4 0.0.0.0 32 | t=0 0 33 | a=charset:Shift_JIS 34 | a=range:npt=0- 35 | a=control:* 36 | a=source-filter:incl IN IP4 239.128.1.1 172.16.10.1 37 | a=control:* 38 | m=video 0 RTP/AVP 98 39 | b=AS:0 40 | a=3GPP-Adaptation-Support:1 41 | a=rtpmap:98 H264/30000 42 | a=control:trackID=2 43 | a=fmtp:98 packetization-mode=1; profile-level-id=42801f; sprop-parameter-sets=J0KAH9oFh8Q=,KM48gA== 44 | m=audio 0 RTP/AVP 97 45 | a=3GPP-Adaptation-Support:1 46 | a=control:trackID=5 47 | a=rtpmap:97 mpeg4-generic/8000/2 48 | a=fmtp:97 streamtype=5; profile-level-id=15; mode=AAC-hbr; config=1410;SizeLength=13; IndexLength=3; IndexDeltaLength=3; CTSDeltaLength=0; DTSDeltaLength=0; 49 | 50 | 51 | SETUP rtsp://10.103.32.109:5001/AAA01.sdp/trackID=2 RTSP/1.0 52 | CSeq: 4 53 | User-Agent: MMP/2.0 (MSIE 8.0; Windows NT 6.1) 54 | Transport: RTP/AVP/TCP;unicast;interleaved=0-1 55 | 56 | RTSP/1.0 200 OK 57 | Server: DSS/6.0.3 (Build/526.3; Platform/Linux; Release/Darwin Streaming Server; State/Development; ) 58 | Cseq: 4 59 | Cache-Control: no-cache 60 | Session: 1725261236621418257 61 | Date: Thu, 12 Apr 2012 14:38:22 GMT 62 | Expires: Thu, 12 Apr 2012 14:38:22 GMT 63 | Transport: RTP/AVP/TCP;unicast;interleaved=0-1 64 | 65 | 66 | SETUP rtsp://10.103.32.109:5001/AAA01.sdp/trackID=5 RTSP/1.0 67 | CSeq: 5 68 | User-Agent: MMP/2.0 (MSIE 8.0; Windows NT 6.1) 69 | Transport: RTP/AVP/TCP;unicast;interleaved=2-3 70 | 71 | RTSP/1.0 200 OK 72 | Server: DSS/6.0.3 (Build/526.3; Platform/Linux; Release/Darwin Streaming Server; State/Development; ) 73 | Cseq: 5 74 | Cache-Control: no-cache 75 | Session: 2550172378129330791 76 | Date: Thu, 12 Apr 2012 14:38:22 GMT 77 | Expires: Thu, 12 Apr 2012 14:38:22 GMT 78 | Transport: RTP/AVP/TCP;unicast;interleaved=2-3 79 | 80 | 81 | PLAY rtsp://10.103.32.109:5001/AAA01.sdp/ RTSP/1.0 82 | CSeq: 6 83 | User-Agent: MMP/2.0 (MSIE 8.0; Windows NT 6.1) 84 | Session: 1725261236621418257 85 | Range: npt=0.000- 86 | 87 | RTSP/1.0 200 OK 88 | Server: DSS/6.0.3 (Build/526.3; Platform/Linux; Release/Darwin Streaming Server; State/Development; ) 89 | Cseq: 6 90 | Session: 1725261236621418257 91 | Range: npt=now- 92 | RTP-Info: url=rtsp://10.103.32.109:5001/AAA01.sdp/trackID=2 93 | 94 | 95 | PLAY rtsp://10.103.32.109:5001/AAA01.sdp/ RTSP/1.0 96 | CSeq: 7 97 | User-Agent: MMP/2.0 (MSIE 8.0; Windows NT 6.1) 98 | Session: 2550172378129330791 99 | Range: npt=0.000- 100 | 101 | RTSP/1.0 200 OK 102 | Server: DSS/6.0.3 (Build/526.3; Platform/Linux; Release/Darwin Streaming Server; State/Development; ) 103 | Cseq: 7 104 | Session: 2550172378129330791 105 | Range: npt=now- 106 | RTP-Info: url=rtsp://10.103.32.109:5001/AAA01.sdp/trackID=5 107 | 108 | 109 | 110 | 111 | 112 | 113 | PAUSE rtsp://10.103.32.109:5001/AAA01.sdp/ RTSP/1.0 114 | CSeq: 8 115 | User-Agent: MMP/2.0 (MSIE 8.0; Windows NT 6.1) 116 | Session: 7894277263700355824 117 | 118 | RTSP/1.0 200 OK 119 | Server: DSS/6.0.3 (Build/526.3; Platform/Linux; Release/Darwin Streaming Server; State/Development; ) 120 | Cseq: 8 121 | Session: 7894277263700355824 122 | 123 | 124 | 125 | 126 | TEARDOWN rtsp://10.103.32.109:5001/AAA01.sdp/ RTSP/1.0 127 | CSeq: 8 128 | User-Agent: MMP/2.0 (MSIE 8.0; Windows NT 6.1) 129 | Session: 2550172378129330791 130 | 131 | RTSP/1.0 200 OK 132 | Server: DSS/6.0.3 (Build/526.3; Platform/Linux; Release/Darwin Streaming Server; State/Development; ) 133 | Cseq: 8 134 | Session: 2550172378129330791 135 | 136 | 137 | TEARDOWN rtsp://10.103.32.109:5001/AAA01.sdp/ RTSP/1.0 138 | CSeq: 9 139 | User-Agent: MMP/2.0 (MSIE 8.0; Windows NT 6.1) 140 | Session: 1725261236621418257 141 | 142 | RTSP/1.0 200 OK 143 | Server: DSS/6.0.3 (Build/526.3; Platform/Linux; Release/Darwin Streaming Server; State/Development; ) 144 | Cseq: 9 145 | Session: 1725261236621418257 -------------------------------------------------------------------------------- /TODO.txt: -------------------------------------------------------------------------------- 1 | Implement an RtspProducer class which does the RTSP ANNOUNCE and 2 | RTSP RECORD process. 3 | 4 | Add support for Basic and Digest Auth 5 | 6 | Add support for UDP transports 7 | 8 | Add support for periodic Get_Parameter calls (Keep ALives) 9 | 10 | Add support for Sender and Receiver Reports RTCP 11 | 12 | Add better support for various RTSP Headers 13 | Transport as an Example 14 | 15 | Improve support for SessionDescription 16 | RtpMap is just the beginning 17 | 18 | Add support for random server response (beyond just data packets) 19 | Example: Session timeouts 20 | 21 | Add Options call to start of connection process to determine if all needed 22 | methods are supported. 23 | 24 | OPTIONS rtsp://10.148.88.111:554/axis-media/media.amp RTSP/1.0 25 | CSeq: 1 26 | User-Agent: Lavf57.83.100 27 | 28 | RTSP/1.0 200 OK 29 | CSeq: 1 30 | Public: OPTIONS, DESCRIBE, ANNOUNCE, GET_PARAMETER, PAUSE, PLAY, RECORD, SETUP, SET_PARAMETER, TEARDOWN 31 | Server: GStreamer RTSP server 32 | Date: Mon, 04 Apr 2022 12:35:49 GMT -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | org.xpertss 8 | rtsp-client 9 | 1.0.2 10 | 11 | 12 | 13 | 14 | GNU General Public License, Version 2.0 15 | https://www.gnu.org/licenses/gpl-2.0.html 16 | repo 17 | 18 | 19 | 20 | 21 | 22 | UTF-8 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | src/test/resources 31 | 32 | 33 | 34 | 35 | 36 | org.apache.maven.plugins 37 | maven-compiler-plugin 38 | 3.1 39 | 40 | 1.8 41 | 1.8 42 | 43 | 44 | 45 | maven-source-plugin 46 | 2.0.4 47 | true 48 | 49 | 50 | attach-sources 51 | package 52 | 53 | jar 54 | test-jar 55 | 56 | 57 | 58 | 59 | 60 | org.apache.maven.plugins 61 | maven-resources-plugin 62 | 2.4.3 63 | 64 | 65 | copy-resources 66 | validate 67 | copy-resources 68 | 69 | true 70 | ${project.build.directory}/classes 71 | 72 | 73 | src/main/resources 74 | true 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | pdf 83 | au 84 | swf 85 | wav 86 | mp3 87 | aac 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | org.slf4j 100 | slf4j-api 101 | 1.7.36 102 | 103 | 104 | org.xpertss 105 | sdp 106 | 1.1.3 107 | 108 | 109 | 110 | 111 | org.slf4j 112 | slf4j-simple 113 | 1.7.36 114 | test 115 | 116 | 117 | junit 118 | junit 119 | 4.13.2 120 | test 121 | 122 | 123 | org.mockito 124 | mockito-all 125 | 1.9.5 126 | test 127 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /src/main/java/xpertss/SourceException.java: -------------------------------------------------------------------------------- 1 | package xpertss; 2 | 3 | import java.net.ProtocolException; 4 | 5 | /** 6 | * A simple exception to encapsulate a RTSP/HTTP response code and 7 | * reason. 8 | */ 9 | public class SourceException extends ProtocolException { 10 | 11 | private int code; 12 | 13 | public SourceException(int code, String reason) 14 | { 15 | super(reason); 16 | this.code = code; 17 | } 18 | 19 | public int getCode() 20 | { 21 | return code; 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/xpertss/media/MediaChannel.java: -------------------------------------------------------------------------------- 1 | package xpertss.media; 2 | 3 | import xpertss.lang.Objects; 4 | import xpertss.lang.Range; 5 | import xpertss.sdp.Attribute; 6 | import xpertss.sdp.MediaDescription; 7 | 8 | /** 9 | * A media channel is a channel setup by the RTSP process to transmit 10 | * or receive media data. 11 | *

12 | * The media channel is intended to convey information to a consumer 13 | * about the channel identifiers used for the media and the type of 14 | * media to expect on those channels. 15 | */ 16 | public class MediaChannel implements Comparable { 17 | 18 | private final MediaDescription desc; 19 | private final Range channels; 20 | 21 | public MediaChannel(MediaDescription desc, Range channels) 22 | { 23 | this.desc = Objects.notNull(desc); 24 | this.channels = Objects.notNull(channels); 25 | } 26 | 27 | /** 28 | * The full media description of the data this channel will 29 | * process. 30 | */ 31 | public MediaDescription getMedia() 32 | { 33 | return desc; 34 | } 35 | 36 | /** 37 | * A control identifier for the channel. 38 | */ 39 | public String getControl() 40 | { 41 | Attribute attr = desc.getAttribute("control"); 42 | return (attr != null) ? attr.getValue() : null; 43 | } 44 | 45 | 46 | /** 47 | * The media type this channel will process. 48 | */ 49 | public MediaType getType() 50 | { 51 | return MediaType.parse(desc); 52 | } 53 | 54 | /** 55 | * The channel identifiers over which this media will be transmitted. 56 | *

57 | * The media data is usually transmitted over the lower of the two 58 | * channel identifiers while control information for the channel is 59 | * transmitted over the higher channel. 60 | */ 61 | public Range getChannels() 62 | { 63 | return channels; 64 | } 65 | 66 | 67 | /** 68 | * Compares this media channel to another based on its channel identifiers. 69 | */ 70 | @Override 71 | public int compareTo(MediaChannel o) 72 | { 73 | return channels.compareTo(o.channels); 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/xpertss/media/MediaConsumer.java: -------------------------------------------------------------------------------- 1 | package xpertss.media; 2 | 3 | import xpertss.sdp.MediaDescription; 4 | import xpertss.sdp.SessionDescription; 5 | 6 | import java.nio.ByteBuffer; 7 | 8 | /** 9 | * A media consumer is responsible for interfacing with an RtspPlayer and 10 | * providing it information about which media streams to setup, and to 11 | * actually process the media streams that result from playback. 12 | */ 13 | public interface MediaConsumer { 14 | 15 | /** 16 | * Given a session description return the media descriptions the 17 | * consumer wishes to be setup for playback. 18 | */ 19 | public MediaDescription[] select(SessionDescription sdp); 20 | 21 | 22 | /** 23 | * For each media description desired this will be called by the 24 | * player to inform it that the channel has been successfully 25 | * setup and to identify the channel identifiers associated with 26 | * it. 27 | */ 28 | public void createChannel(MediaChannel channel); 29 | 30 | /** 31 | * Called to indicate that the previously created channels should 32 | * be destroyed as they are no longer valid. 33 | */ 34 | public void destroyChannels(); 35 | 36 | 37 | 38 | /** 39 | * Once playback has begun the actual channel data will be delivered 40 | * to the consumer via this method. The channelId will be one of the 41 | * identifiers previously communicated to the newChannel callback. 42 | */ 43 | public void consume(int channelId, ByteBuffer data); 44 | 45 | 46 | /** 47 | * Called to notify the consumer of an error that has terminated 48 | * playback. 49 | */ 50 | public void handle(Throwable t); 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/xpertss/media/MediaType.java: -------------------------------------------------------------------------------- 1 | package xpertss.media; 2 | 3 | import xpertss.sdp.MediaDescription; 4 | 5 | /** 6 | * An Enumeration of Standard SDP Media Types. 7 | */ 8 | public enum MediaType { 9 | 10 | Audio, Video, Text, Application, Message; 11 | 12 | 13 | public static MediaType parse(MediaDescription desc) 14 | { 15 | String mediaType = desc.getMedia().getType(); 16 | if("Audio".equalsIgnoreCase(mediaType)) { 17 | return Audio; 18 | } else if("Video".equalsIgnoreCase(mediaType)) { 19 | return Video; 20 | } else if("Text".equalsIgnoreCase(mediaType)) { 21 | return Text; 22 | } else if("Application".equalsIgnoreCase(mediaType)) { 23 | return Application; 24 | } else if("Message".equalsIgnoreCase(mediaType)) { 25 | return Message; 26 | } 27 | return null; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/xpertss/mime/Header.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright XpertSoftware All rights reserved. 3 | * 4 | * Date: 3/15/11 1:08 PM 5 | */ 6 | package xpertss.mime; 7 | 8 | /** 9 | * Represents a Mime header field. 10 | *

11 | * The Mime header fields follow the format defined by Section 3.1 of RFC 822. 12 | * Each header field consists of a name followed by a colon (":") and the field 13 | * value. Field names are case-insensitive. The field value MAY be preceded by 14 | * any amount of LWS, though a single SP is preferred. 15 | * 16 | *

 17 |  *     message-header = field-name ":" [ field-value ]
 18 |  *     field-name     = token
 19 |  *     field-value    = *( field-content | LWS )
 20 |  *     field-content  = <the OCTETs making up the field-value
 21 |  *                      and consisting of either *TEXT or combinations
 22 |  *                      of token, separators, and quoted-string>
 23 |  *

24 | * A Header may represent a single value or multiple values. Each value may be 25 | * either named or simple. Additionally, each value may or may not have associated 26 | * parameters which themselves may be named or not. This interface attempts to 27 | * define a means to access all of the various header parts. 28 | */ 29 | public interface Header { 30 | 31 | /** 32 | * Http defines four types of headers. All headers not specifically 33 | * defined to be general, request, or response are treated as entity 34 | * headers. 35 | */ 36 | public enum Type { 37 | /** 38 | * A general header is one that may be included in either the request or 39 | * response and is applied to the overall message itself. 40 | */ 41 | General, 42 | 43 | /** 44 | * A request header is applied only to impl requests and applies to the 45 | * message overall. 46 | */ 47 | Request, 48 | 49 | /** 50 | * A response header is applied only to impl responses and applies to the 51 | * message overall. 52 | */ 53 | Response, 54 | 55 | /** 56 | * An entity header defines the entity contained within the message. 57 | */ 58 | Entity, 59 | 60 | /** 61 | * A raw header is any header for which a defined parser was not found. 62 | * These are typically non-standard headers and will always return an 63 | * unparsed raw string when getValue is called. 64 | */ 65 | Raw 66 | } 67 | 68 | 69 | /** 70 | * Returns the header name as a String. 71 | */ 72 | public String getName(); 73 | 74 | /** 75 | * Returns a fully formatted value line including all values and their 76 | * associated parameters. This method provides an easier means of access 77 | * to simple values. 78 | */ 79 | public String getValue(); 80 | 81 | /** 82 | * Returns the header type this header represents. 83 | */ 84 | public Type getType(); 85 | 86 | 87 | /** 88 | * Returns the number of values this header contains. 89 | */ 90 | public int size(); 91 | 92 | /** 93 | * Returns the header value at the specified index. 94 | */ 95 | public HeaderValue getValue(int index); 96 | 97 | /** 98 | * Returns the specified named header value if it exists. 99 | */ 100 | public HeaderValue getValue(String name); 101 | 102 | 103 | /** 104 | * Return a formatted string representation of this header and its values 105 | * and parameters. 106 | */ 107 | public String toString(); 108 | 109 | 110 | } 111 | -------------------------------------------------------------------------------- /src/main/java/xpertss/mime/HeaderParser.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright XpertSoftware All rights reserved. 3 | * 4 | * Date: 3/18/11 11:09 AM 5 | */ 6 | package xpertss.mime; 7 | 8 | import xpertss.mime.spi.HeaderParserProvider; 9 | 10 | import java.util.ServiceLoader; 11 | 12 | /** 13 | * HeaderParser will parse MIME headers into Header objects. 14 | *

15 | * This utilizes a ServiceProvider framework where a parser for each header 16 | * is loaded to actually parse the header contents. This class simply parses 17 | * the header name so that it may be used to locate a suitable value parser. 18 | * 19 | * @see xpertss.mime.spi.HeaderParserProvider 20 | */ 21 | public abstract class HeaderParser { 22 | 23 | private static ServiceLoader loader = 24 | ServiceLoader.load(HeaderParserProvider.class, HeaderParserProvider.class.getClassLoader()); 25 | 26 | 27 | /** 28 | * Parse a raw header and return an appropriate Header object representation. 29 | * 30 | * @param raw The raw header string from a request or response 31 | * @return A header object instance 32 | * @throws MalformedException If the header was malformed or incomplete 33 | * @throws NullPointerException If the raw header is {@code null} 34 | */ 35 | public static Header parse(CharSequence raw) throws MalformedException 36 | { 37 | if(raw == null) throw new NullPointerException("raw can not be null"); 38 | HeaderTokenizer h = new HeaderTokenizer(raw, HeaderTokenizer.MIME); 39 | HeaderTokenizer.Token token = h.next(); 40 | if(token.getType() != HeaderTokenizer.Token.ATOM) throw new MalformedException("expected header name"); 41 | String name = token.getValue(); 42 | if((token = h.next()).getType() != ':') throw new MalformedException("expected header delimiter"); 43 | if((token = h.next()).getType() != HeaderTokenizer.Token.LWS) throw new MalformedException("expected header separator"); 44 | return parse(name, h.getRemainder()); 45 | } 46 | 47 | /** 48 | * Parse the given header value for the specified header name and return an 49 | * appropriate Header objects representation. 50 | * 51 | * @param name The name of the header 52 | * @param rawValue A string representing the raw value to be parsed 53 | * @return A header object instance 54 | * @throws MalformedException If the header value was malformed or incomplete 55 | * @throws NullPointerException If either the header or rawValue values are {@code null} 56 | */ 57 | public static Header parse(String name, CharSequence rawValue) throws MalformedException 58 | { 59 | if(name == null) throw new NullPointerException("name can not be null"); 60 | if(rawValue == null) throw new NullPointerException("rawValue can not be null"); 61 | // revert to the service providers. 62 | for(HeaderParserProvider provider: loader) { 63 | HeaderParser parser = provider.create(name); 64 | if(parser != null) return parser.doParse(rawValue); 65 | } 66 | return new RawHeader(name, rawValue); // default to a RawHeader as nothing knows how to parse it 67 | } 68 | 69 | 70 | /** 71 | * Service provider implementations should implement this method to parse a 72 | * raw header value into a Header object. 73 | * 74 | * @param raw The raw header value to parse. 75 | * @return A header object representing the parsed header value 76 | * @throws MalformedException If the header value was malformed or incomplete 77 | */ 78 | protected abstract Header doParse(CharSequence raw) throws MalformedException; 79 | 80 | private static class RawHeader implements Header { 81 | 82 | private String name; 83 | private String value; 84 | 85 | private RawHeader(String name, CharSequence value) 86 | { 87 | this.name = name; 88 | this.value = value.toString(); 89 | } 90 | 91 | public String getName() 92 | { 93 | return name; 94 | } 95 | 96 | public String getValue() 97 | { 98 | return value; 99 | } 100 | 101 | public Type getType() 102 | { 103 | return Type.Raw; 104 | } 105 | 106 | public int size() 107 | { 108 | return 0; 109 | } 110 | 111 | public HeaderValue getValue(int index) 112 | { 113 | return null; 114 | } 115 | 116 | public HeaderValue getValue(String name) 117 | { 118 | return null; 119 | } 120 | 121 | public String toString() 122 | { 123 | return name + ": " + value; 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/main/java/xpertss/mime/HeaderTokenizer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright XpertSoftware All rights reserved. 3 | * 4 | * Created on Mar 6, 2006 5 | */ 6 | package xpertss.mime; 7 | 8 | import xpertss.lang.Objects; 9 | 10 | /** 11 | * This class tokenizes RFC822 and MIME headers into the basic symbols specified 12 | * by RFC822 and MIME. 13 | *

14 | * This class handles folded headers (ie headers with embedded CRLF SPACE sequences). 15 | * The folds are removed in the returned tokens. 16 | * 17 | * @author cfloersch 18 | */ 19 | public class HeaderTokenizer { 20 | 21 | /** 22 | * The Token class represents tokens returned by the 23 | * HeaderTokenizer. 24 | */ 25 | public static class Token { 26 | 27 | private int type; 28 | private CharSequence value; 29 | 30 | /** 31 | * Token type indicating an ATOM. 32 | */ 33 | public static final int ATOM = -1; 34 | 35 | /** 36 | * Token type indicating a quoted string. The value 37 | * field contains the string without the quotes. 38 | */ 39 | public static final int QUOTEDSTRING = -2; 40 | 41 | /** 42 | * Token type indicating a comment. The value field 43 | * contains the comment string without the comment 44 | * start and end symbols. 45 | */ 46 | public static final int COMMENT = -3; 47 | 48 | /** 49 | * Token type indicating linear whitespace. 50 | */ 51 | public static final int LWS = -4; 52 | 53 | /** 54 | * Token type indicating end of input. 55 | */ 56 | public static final int EOF = -5; 57 | 58 | /** 59 | * Constructor. 60 | * 61 | * @param type Token type 62 | * @param value Token value 63 | */ 64 | public Token(int type, CharSequence value) 65 | { 66 | this.type = type; 67 | this.value = value; 68 | } 69 | 70 | /** 71 | * Return the type of the token. If the token represents a 72 | * delimiter or a control character, the type is that character 73 | * itself, converted to an integer. Otherwise, it's value is 74 | * one of the following: 75 | *

    76 | *
  • ATOM A sequence of ASCII characters 77 | * delimited by either SPACE, CTL, "(", <"> or the 78 | * specified SPECIALS 79 | *
  • QUOTEDSTRING A sequence of ASCII characters 80 | * within quotes 81 | *
  • COMMENT A sequence of ASCII characters 82 | * within "(" and ")". 83 | *
  • EOF End of header 84 | *
85 | */ 86 | public int getType() 87 | { 88 | return type; 89 | } 90 | 91 | /** 92 | * Returns the value of the token just read. When the current 93 | * token is a quoted string, this field contains the body of the 94 | * string, without the quotes. When the current token is a comment, 95 | * this field contains the body of the comment. 96 | * 97 | * @return token value 98 | */ 99 | public String getValue() 100 | { 101 | return value.toString(); 102 | } 103 | 104 | public boolean equals(Object obj) 105 | { 106 | if(obj instanceof Token) { 107 | Token other = (Token) obj; 108 | return type == other.type && eq(value, other.value); 109 | } 110 | return false; 111 | } 112 | 113 | public int hashCode() 114 | { 115 | int valHash = (value != null) ? value.hashCode() : 0; 116 | return type ^ valHash; 117 | } 118 | 119 | private boolean eq(Object one, Object two) 120 | { 121 | return (one == null) ? two == null : one.equals(two); 122 | } 123 | } 124 | 125 | private CharSequence string; // the string to be tokenized 126 | private String delimiters; // delimiter string 127 | private int currentPos; // current parse position 128 | private int maxPos; // string length 129 | private int nextPos; // track start of next Token for next() 130 | private int peekPos; // track start of next Token for peek() 131 | 132 | /** 133 | * RFC822 specials 134 | */ 135 | public final static String RFC822 = "()<>@,;:\\\"\t .[]"; 136 | 137 | /** 138 | * MIME specials 139 | */ 140 | public final static String MIME = "()<>@,;:\\\"\t []/?="; 141 | 142 | // The EOF Token 143 | private final static Token EOFToken = new Token(Token.EOF, null); 144 | 145 | /** 146 | * Constructor that takes a rfc822 style header. 147 | * 148 | * @param header The rfc822 header to be tokenized 149 | * @param delimiters Set of delimiter characters 150 | * to be used to delimit ATOMS. These 151 | * are usually RFC822 or 152 | * MIME 153 | */ 154 | public HeaderTokenizer(CharSequence header, String delimiters) 155 | { 156 | string = Objects.ifNull(header, ""); 157 | this.delimiters = Objects.ifNull(delimiters, RFC822); 158 | currentPos = nextPos = peekPos = 0; 159 | maxPos = string.length(); 160 | } 161 | 162 | 163 | /** 164 | * Constructor. The RFC822 defined delimiters - RFC822 - are 165 | * used to delimit ATOMS. Also comments are skipped and not 166 | * returned as tokens 167 | */ 168 | public HeaderTokenizer(String header) 169 | { 170 | this(header, RFC822); 171 | } 172 | 173 | 174 | 175 | 176 | public int position() 177 | { 178 | return currentPos; 179 | } 180 | 181 | 182 | /** 183 | * Parses the next token from this String.

184 | *

185 | * Clients sit in a loop calling next() to parse successive 186 | * tokens until an EOF Token is returned. 187 | * 188 | * @return the next Token 189 | * @throws MalformedException if the parse fails 190 | */ 191 | public Token next() throws MalformedException 192 | { 193 | currentPos = nextPos; // setup currentPos 194 | Token tk = getNext(); 195 | nextPos = peekPos = currentPos; // update currentPos and peekPos 196 | return tk; 197 | } 198 | 199 | 200 | /** 201 | * Peek at the next token, without actually removing the token 202 | * from the parse stream. Invoking this method multiple times 203 | * will return successive tokens, until next() is 204 | * called.

205 | * 206 | * @return the next Token 207 | * @throws MalformedException if the parse fails 208 | */ 209 | public Token peek() throws MalformedException 210 | { 211 | currentPos = peekPos; // setup currentPos 212 | Token tk = getNext(); 213 | peekPos = currentPos; // update peekPos 214 | return tk; 215 | } 216 | 217 | 218 | /** 219 | * Return the rest of the Header. 220 | * 221 | * @return String rest of header. null is returned if we are 222 | * already at end of header 223 | */ 224 | public String getRemainder() 225 | { 226 | return string.subSequence(nextPos, string.length()).toString(); 227 | } 228 | 229 | 230 | /* 231 | * Return the next token starting from 'currentPos'. After the 232 | * parse, 'currentPos' is updated to point to the start of the 233 | * next token. 234 | */ 235 | 236 | private Token getNext() throws MalformedException 237 | { 238 | // If we're already at end of string, return EOF 239 | if(currentPos >= maxPos) 240 | return EOFToken; 241 | 242 | char c; 243 | int start; 244 | boolean filter = false; 245 | c = string.charAt(currentPos); 246 | 247 | // Check for whitespace 248 | if(isWhiteSpace(c)) { 249 | for(start = currentPos; currentPos < maxPos; currentPos++) { 250 | c = string.charAt(currentPos); 251 | if(!isWhiteSpace(c)) break; 252 | } 253 | return new Token(Token.LWS, string.subSequence(start, currentPos)); 254 | } 255 | 256 | // Check for comments and position currentPos 257 | // beyond the comment 258 | if(c == '(') { 259 | // Parsing comment .. 260 | int nesting; 261 | for(start = ++currentPos, nesting = 1; nesting > 0 && currentPos < maxPos; currentPos++) { 262 | c = string.charAt(currentPos); 263 | if(c == '\\') { // Escape sequence 264 | currentPos++; // skip the escaped character 265 | filter = true; 266 | } else if(c == '\r') { 267 | filter = true; 268 | } else if(c == '(') { 269 | nesting++; 270 | } else if(c == ')') { 271 | nesting--; 272 | } 273 | } 274 | if(nesting != 0) 275 | throw new MalformedException("Unbalanced comments"); 276 | // Return the comment, if we are asked to. 277 | // Note that the comment start & end markers are ignored. 278 | CharSequence s; 279 | if(filter) 280 | s = filterToken(string, start, currentPos - 1); 281 | else 282 | s = string.subSequence(start, currentPos - 1); 283 | return new Token(Token.COMMENT, s); 284 | } 285 | 286 | // Check for quoted-string and position currentPos 287 | // beyond the terminating quote 288 | if(c == '"') { 289 | for(start = ++currentPos; currentPos < maxPos; currentPos++) { 290 | c = string.charAt(currentPos); 291 | if(c == '\\') { // Escape sequence 292 | currentPos++; 293 | filter = true; 294 | } else if(c == '\r') { 295 | filter = true; 296 | } else if(c == '"') { 297 | currentPos++; 298 | CharSequence s; 299 | if(filter) 300 | s = filterToken(string, start, currentPos - 1); 301 | else 302 | s = string.subSequence(start, currentPos - 1); 303 | return new Token(Token.QUOTEDSTRING, s); 304 | } 305 | } 306 | throw new MalformedException("Unbalanced quoted string"); 307 | } 308 | 309 | // Check for SPECIAL or CTL 310 | if(c < 040 || c >= 0177 || delimiters.indexOf(c) >= 0) { 311 | currentPos++; // re-position currentPos 312 | char[] ch = new char[1]; 313 | ch[0] = c; 314 | return new Token((int) c, new String(ch)); 315 | } 316 | 317 | // Check for ATOM 318 | for(start = currentPos; currentPos < maxPos; currentPos++) { 319 | c = string.charAt(currentPos); 320 | // ATOM is delimited by either SPACE, CTL, "(", <"> 321 | // or the specified SPECIALS 322 | if(c < 040 || c >= 0177 || c == '(' || c == ' ' || c == '"' || delimiters.indexOf(c) >= 0) 323 | break; 324 | } 325 | return new Token(Token.ATOM, string.subSequence(start, currentPos)); 326 | } 327 | 328 | 329 | // Skip SPACE, HT, CR and NL 330 | 331 | private int skipWhiteSpace() 332 | { 333 | char c; 334 | for(; currentPos < maxPos; currentPos++) { 335 | if(((c = string.charAt(currentPos)) != '\t') && (c != ' ') && (c != '\r') && (c != '\n')) 336 | return currentPos; 337 | } 338 | return Token.EOF; 339 | } 340 | 341 | private boolean isWhiteSpace(char c) 342 | { 343 | return (c == '\t' || c == '\r' || c == '\n' || c == ' '); 344 | } 345 | 346 | 347 | /* Process escape sequences and embedded LWSPs from a comment or 348 | * quoted string. 349 | */ 350 | 351 | private static CharSequence filterToken(CharSequence s, int start, int end) 352 | { 353 | StringBuffer sb = new StringBuffer(); 354 | char c; 355 | boolean gotEscape = false; 356 | boolean gotCR = false; 357 | 358 | for(int i = start; i < end; i++) { 359 | c = s.charAt(i); 360 | if(c == '\n' && gotCR) { 361 | // This LF is part of an unescaped 362 | // CRLF sequence (i.e, LWSP). Skip it. 363 | gotCR = false; 364 | continue; 365 | } 366 | 367 | gotCR = false; 368 | if(!gotEscape) { 369 | // Previous character was NOT '\' 370 | if(c == '\\') // skip this character 371 | gotEscape = true; 372 | else if(c == '\r') // skip this character 373 | gotCR = true; 374 | else // append this character 375 | sb.append(c); 376 | } else { 377 | // Previous character was '\'. So no need to 378 | // bother with any special processing, just 379 | // append this character 380 | sb.append(c); 381 | gotEscape = false; 382 | } 383 | } 384 | return sb; 385 | } 386 | 387 | 388 | } 389 | -------------------------------------------------------------------------------- /src/main/java/xpertss/mime/HeaderValue.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright XpertSoftware All rights reserved. 3 | * 4 | * Date: 3/17/11 10:19 AM 5 | */ 6 | package xpertss.mime; 7 | 8 | /** 9 | * Many HTTP headers support the concept of multiple values separated by commas. 10 | * A HeaderValue represents just one of those values along with its associated 11 | * parameters. 12 | *

13 | * Example of simple multi-valued headers: 14 | *

15 |  *    If-Match: "xyzzy", "r2d2xxxx", "c3piozzzz"
16 |  *    Upgrade: HTTP/2.0, SHTTP/1.3, IRC/6.9, RTA/x11
17 |  * 

18 | * An individual header value may be simple as shown above or it may be a named 19 | * value as in the following example: 20 | *

21 |  *    Cache-Control: max-age=0
22 |  * 

23 | * Additionally, a header may combine simple and named values as follows: 24 | *

25 |  *    Cache-Control: private, max-age=0
26 |  * 

27 | * In the example the simple header value will have a {@code null} name field and 28 | * the value field will have the value private. The named value will have a name 29 | * field of max-age and a value field of 0. 30 | * 31 | */ 32 | public interface HeaderValue { 33 | 34 | 35 | /** 36 | * Ths will return the value's name or {@code null} if it is not a 37 | * named value. 38 | */ 39 | public String getName(); 40 | 41 | /** 42 | * This will return the value's value. 43 | */ 44 | public String getValue(); 45 | 46 | 47 | 48 | 49 | 50 | /** 51 | * Returns the number of parameters this header value contains. 52 | */ 53 | public int size(); 54 | 55 | /** 56 | * Returns the parameter at the specified index. 57 | */ 58 | public Parameter getParameter(int index); 59 | 60 | /** 61 | * Returns the specified named parameter if it exists. 62 | */ 63 | public Parameter getParameter(String name); 64 | 65 | 66 | 67 | /** 68 | * Return a formatted string representation of this header value and its 69 | * parameters. 70 | */ 71 | public String toString(); 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/xpertss/mime/Headers.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright XpertSoftware All rights reserved. 3 | * 4 | * Date: 4/6/11 2:50 PM 5 | */ 6 | package xpertss.mime; 7 | 8 | import xpertss.function.Predicates; 9 | import xpertss.lang.Integers; 10 | import xpertss.lang.Objects; 11 | import xpertss.lang.Strings; 12 | import xpertss.utils.Utils; 13 | 14 | import java.util.ArrayList; 15 | import java.util.Enumeration; 16 | import java.util.List; 17 | import java.util.NoSuchElementException; 18 | import java.util.function.Predicate; 19 | 20 | 21 | /** 22 | * Headers is a utility class that manages RFC822 style headers. 23 | *

24 | *


A note on RFC822 and MIME headers

25 | *

26 | * RFC822 and MIME header fields must contain only 27 | * US-ASCII characters. If a header contains non US-ASCII characters, 28 | * it must be encoded as per the rules in RFC 2047. Callers of the 29 | * setHeader and addHeader methods are 30 | * responsible for enforcing the MIME requirements for the specified 31 | * header. 32 | */ 33 | public class Headers { 34 | 35 | public enum Type { 36 | Http, Rtsp, Mail 37 | } 38 | 39 | 40 | private List

headers; 41 | private boolean readOnly; 42 | 43 | public Headers(Type type) 44 | { 45 | headers = new ArrayList
(); 46 | 47 | if (Type.Mail == type) { 48 | headers.add(new PositionHeader("Return-Path")); 49 | headers.add(new PositionHeader("Received")); 50 | headers.add(new PositionHeader("Message-ID")); 51 | headers.add(new PositionHeader("Resent-Date")); 52 | headers.add(new PositionHeader("Date")); 53 | headers.add(new PositionHeader("Resent-From")); 54 | headers.add(new PositionHeader("From")); 55 | headers.add(new PositionHeader("Sender")); 56 | headers.add(new PositionHeader("Reply-To")); 57 | headers.add(new PositionHeader("To")); 58 | headers.add(new PositionHeader("Subject")); 59 | headers.add(new PositionHeader("Cc")); 60 | headers.add(new PositionHeader("In-Reply-To")); 61 | headers.add(new PositionHeader("Resent-Message-ID")); 62 | headers.add(new PositionHeader("Errors-To")); 63 | headers.add(new PositionHeader("Mime-Version")); 64 | headers.add(new PositionHeader("Content-Type")); 65 | headers.add(new PositionHeader("Content-Transfer-Encoding")); 66 | headers.add(new PositionHeader("Content-MD5")); 67 | headers.add(new PositionHeader(":")); 68 | headers.add(new PositionHeader("Content-Length")); 69 | headers.add(new PositionHeader("Status")); 70 | } else if(type == Type.Http) { 71 | headers.add(new PositionHeader("Host")); 72 | headers.add(new PositionHeader("User-Agent")); 73 | headers.add(new PositionHeader("Date")); 74 | 75 | headers.add(new PositionHeader("Accept")); 76 | headers.add(new PositionHeader("Accept-Language")); 77 | headers.add(new PositionHeader("Accept-Encoding")); 78 | headers.add(new PositionHeader("Accept-Charset")); 79 | 80 | headers.add(new PositionHeader("Authorization")); 81 | headers.add(new PositionHeader("Proxy-Authorization")); 82 | 83 | headers.add(new PositionHeader("Referer")); 84 | headers.add(new PositionHeader("If-Modified-Since")); 85 | headers.add(new PositionHeader("If-None-Match")); 86 | 87 | headers.add(new PositionHeader("Transfer-Encoding")); 88 | 89 | headers.add(new PositionHeader("Content-Disposition")); 90 | headers.add(new PositionHeader("Content-Type")); 91 | headers.add(new PositionHeader("Content-Location")); 92 | headers.add(new PositionHeader("Content-MD5")); 93 | headers.add(new PositionHeader("Content-Language")); 94 | headers.add(new PositionHeader("Content-Encoding")); 95 | headers.add(new PositionHeader("Content-Length")); 96 | headers.add(new PositionHeader(":")); 97 | 98 | headers.add(new PositionHeader("Proxy-Connection")); 99 | headers.add(new PositionHeader("Connection")); 100 | headers.add(new PositionHeader("Pragma")); 101 | headers.add(new PositionHeader("Cache-Control")); 102 | } else if(type == Type.Rtsp) { 103 | headers.add(new PositionHeader("Server")); 104 | headers.add(new PositionHeader("CSeq")); 105 | headers.add(new PositionHeader("User-Agent")); 106 | headers.add(new PositionHeader("Date")); 107 | headers.add(new PositionHeader("Expires")); 108 | 109 | headers.add(new PositionHeader("Accept")); 110 | headers.add(new PositionHeader("Accept-Language")); 111 | headers.add(new PositionHeader("Accept-Encoding")); 112 | 113 | headers.add(new PositionHeader("Authorization")); 114 | 115 | headers.add(new PositionHeader("Referer")); 116 | headers.add(new PositionHeader("If-Modified-Since")); 117 | headers.add(new PositionHeader("If-Match")); 118 | headers.add(new PositionHeader("Last-Modified")); 119 | 120 | headers.add(new PositionHeader("Transport")); 121 | headers.add(new PositionHeader("Session")); 122 | headers.add(new PositionHeader("RTP-Info")); 123 | headers.add(new PositionHeader("Location")); 124 | headers.add(new PositionHeader("Range")); 125 | 126 | 127 | headers.add(new PositionHeader("Content-Base")); 128 | headers.add(new PositionHeader("Content-Type")); 129 | headers.add(new PositionHeader("Content-Location")); 130 | headers.add(new PositionHeader("Content-Language")); 131 | headers.add(new PositionHeader("Content-Encoding")); 132 | headers.add(new PositionHeader("Content-Length")); 133 | headers.add(new PositionHeader(":")); 134 | 135 | headers.add(new PositionHeader("Connection")); 136 | headers.add(new PositionHeader("Cache-Control")); 137 | 138 | } 139 | } 140 | 141 | private Headers(List
headers) 142 | { 143 | this.headers = Objects.notNull(headers); 144 | this.readOnly = true; 145 | } 146 | 147 | 148 | /** 149 | * Return an enumeration of the headers in their predefined order. 150 | */ 151 | public Enumeration
headers() 152 | { 153 | Predicate filter = Predicates.instanceOf(PositionHeader.class); 154 | return new FilteringEnumeration(headers, filter); 155 | } 156 | 157 | public boolean contains(String name) 158 | { 159 | return getHeader(name) != null; 160 | } 161 | 162 | 163 | /** 164 | * This will return the first header with the given name. 165 | */ 166 | public Header getHeader(String name) 167 | { 168 | for(Header header : headers) { 169 | if(Strings.equalIgnoreCase(header.getName(), name)) { 170 | if(!(header instanceof PositionHeader)) return header; 171 | } 172 | } 173 | return null; 174 | } 175 | 176 | /** 177 | * This will return all the header with the given name in the order they 178 | * were found. 179 | */ 180 | public Header[] getHeaders(String name) 181 | { 182 | List
results = new ArrayList
(); 183 | for(Header header : headers) { 184 | if(Strings.equalIgnoreCase(header.getName(), name)) { 185 | if(!(header instanceof PositionHeader)) results.add(header); 186 | } 187 | } 188 | return results.toArray(new Header[results.size()]); 189 | } 190 | 191 | 192 | /** 193 | * This will add the named header to the end of the set of existing 194 | * headers with the same name or its default position within the set 195 | * if no header with the same name pre-exists. 196 | * 197 | * @throws MalformedException If the header name or value are malformed 198 | */ 199 | public void addHeader(String name, String value) throws MalformedException 200 | { 201 | if(readOnly) throw new IllegalStateException("src are readonly"); 202 | Header newHeader = HeaderParser.parse(name, value); 203 | int pos = 0; 204 | for(int i = headers.size() - 1; i >= 0; i--) { 205 | Header header = headers.get(i); 206 | if(Strings.equalIgnoreCase(header.getName(), name)) { 207 | headers.add(i + 1, newHeader); 208 | return; 209 | } else if(header.getName().equals(":")) { 210 | pos = i; 211 | } 212 | } 213 | headers.add(pos + 1, newHeader); 214 | } 215 | 216 | /** 217 | * This will add the named header to its default position within the set 218 | * removing any previously existing headers with the same name. 219 | * 220 | * @throws MalformedException If the header name or value are malformed 221 | */ 222 | public void setHeader(String name, String value) throws MalformedException 223 | { 224 | if(readOnly) throw new IllegalStateException("src are readonly"); 225 | Header newHeader = HeaderParser.parse(name, value); 226 | int pos = 0; 227 | for(int i = headers.size() - 1; i >= 0; i--) { 228 | Header header = headers.get(i); 229 | if(Strings.equalIgnoreCase(header.getName(), name)) { 230 | if(header instanceof PositionHeader) { 231 | headers.add(i + 1, newHeader); 232 | return; 233 | } else { 234 | headers.remove(i); 235 | } 236 | } else if(Strings.equal(header.getName(), ":")) { 237 | pos = i; 238 | } 239 | } 240 | headers.add(pos + 1, newHeader); 241 | } 242 | 243 | 244 | /** 245 | * This will add the named header to its default position within the set 246 | * if and only if there are no existing headers with the given name. 247 | * 248 | * @throws MalformedException If the header name or value are malformed 249 | */ 250 | public boolean setIfNotSet(String name, String value) throws MalformedException 251 | { 252 | if(readOnly) throw new IllegalStateException("src are readonly"); 253 | Header newHeader = HeaderParser.parse(name, value); 254 | int pos = 0; 255 | for(int i = headers.size() - 1; i >= 0; i--) { 256 | Header header = headers.get(i); 257 | if(Strings.equalIgnoreCase(header.getName(), name)) { 258 | if(header instanceof PositionHeader) { 259 | headers.add(i + 1, newHeader); 260 | return true; 261 | } else{ 262 | return false; 263 | } 264 | } else if(Strings.equal(header.getName(), ":")) { 265 | pos = i; 266 | } 267 | } 268 | headers.add(pos + 1, newHeader); 269 | return true; 270 | } 271 | 272 | /** 273 | * This will remove all src with the given name returning the number 274 | * removed. 275 | */ 276 | public int remove(String name) 277 | { 278 | int count = 0; 279 | for(int i = headers.size() - 1; i >= 0; i--) { 280 | Header header = headers.get(i); 281 | if(Strings.equalIgnoreCase(header.getName(), name)) { 282 | if(header instanceof PositionHeader) { 283 | return count; 284 | } else { 285 | headers.remove(i); 286 | count++; 287 | } 288 | } else if(count > 0) { 289 | return count; 290 | } 291 | } 292 | return count; 293 | } 294 | 295 | 296 | 297 | 298 | // Helpers 299 | 300 | /** 301 | * Helper method to retrieve the Content-Length header value as a long. 302 | *

303 | * This will return -1 if the Content-Header header is not defined. 304 | */ 305 | public long getContentLength() 306 | { 307 | Header contentLength = getHeader("Content-Length"); 308 | return Integers.parse(toString(contentLength), -1); 309 | } 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | @Override 320 | public boolean equals(Object obj) 321 | { 322 | if(obj instanceof Headers) { 323 | Headers o = (Headers) obj; 324 | return Objects.equal(headers, o.headers); 325 | } 326 | return false; 327 | } 328 | 329 | @Override 330 | public int hashCode() 331 | { 332 | return Objects.hash(headers); 333 | } 334 | 335 | 336 | /** 337 | * Returns the set of headers with one header per line 338 | */ 339 | @Override 340 | public String toString() 341 | { 342 | StringBuilder builder = new StringBuilder(); 343 | for(Enumeration

e = headers(); e.hasMoreElements(); ) { 344 | builder.append(e.nextElement()).append("\r\n"); 345 | } 346 | return builder.toString(); 347 | } 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | public static Headers create(List raw) 361 | { 362 | List
headers = new ArrayList
(); 363 | StringBuilder header = new StringBuilder(); 364 | for(int i = raw.size() - 1; i >= 0; i--) { 365 | header.insert(0, raw.get(i)); 366 | if(!Utils.isWhiteSpace(header.charAt(0))) { 367 | headers.add(0, HeaderParser.parse(Utils.getAndClear(header))); 368 | } 369 | } 370 | return new Headers(headers); 371 | } 372 | 373 | public static String toString(Header header) 374 | { 375 | return (header == null) ? null : header.getValue(); 376 | } 377 | 378 | 379 | 380 | private static class FilteringEnumeration implements Enumeration
{ 381 | 382 | private final List
src; 383 | private final Predicate filter; 384 | 385 | private Header next; 386 | private int i; 387 | 388 | private FilteringEnumeration(List
headers, Predicate filter) 389 | { 390 | this.src = Objects.notNull(headers); 391 | this.filter = Objects.notNull(filter); 392 | this.next = findNext(); 393 | } 394 | 395 | 396 | @Override public boolean hasMoreElements() 397 | { 398 | return next != null; 399 | } 400 | 401 | @Override public Header nextElement() 402 | { 403 | if(next == null) { 404 | throw new NoSuchElementException("enumeration exhausted"); 405 | } 406 | Header result = next; 407 | next = findNext(); 408 | return result; 409 | } 410 | 411 | private Header findNext() 412 | { 413 | while(i < src.size()) { 414 | Header header = src.get(i++); 415 | if(!filter.test(header)) return header; 416 | } 417 | return null; 418 | } 419 | } 420 | 421 | private static class PositionHeader implements Header { 422 | 423 | private final String name; 424 | 425 | private PositionHeader(String name) 426 | { 427 | this.name = Objects.notNull(name); 428 | } 429 | 430 | @Override 431 | public String getName() 432 | { 433 | return name; 434 | } 435 | 436 | @Override 437 | public String getValue() 438 | { 439 | return null; 440 | } 441 | 442 | @Override 443 | public Type getType() 444 | { 445 | return Type.Raw; 446 | } 447 | 448 | @Override 449 | public int size() 450 | { 451 | return 0; 452 | } 453 | 454 | @Override 455 | public HeaderValue getValue(int index) 456 | { 457 | return null; 458 | } 459 | 460 | @Override 461 | public HeaderValue getValue(String name) 462 | { 463 | return null; 464 | } 465 | 466 | } 467 | } 468 | -------------------------------------------------------------------------------- /src/main/java/xpertss/mime/MalformedException.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright XpertSoftware All rights reserved. 3 | * 4 | * Date: 3/21/11 11:09 AM 5 | */ 6 | package xpertss.mime; 7 | 8 | /** 9 | * Thrown to indicate that a header was Malformed and could not be parsed. 10 | */ 11 | public class MalformedException extends RuntimeException { 12 | 13 | 14 | /** 15 | * No message constructor. 16 | */ 17 | public MalformedException() 18 | { 19 | super(); 20 | } 21 | 22 | public MalformedException(String message) 23 | { 24 | super(message); 25 | } 26 | 27 | public MalformedException(String message, Throwable cause) 28 | { 29 | super(message, cause); 30 | } 31 | 32 | public MalformedException(Throwable cause) 33 | { 34 | super(cause); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/xpertss/mime/MimeFlavor.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright XpertSoftware All rights reserved. 3 | * 4 | * Date: 4/7/11 12:19 PM 5 | */ 6 | package xpertss.mime; 7 | 8 | import org.w3c.dom.Document; 9 | import xpertss.lang.Objects; 10 | 11 | import java.io.File; 12 | import java.io.InputStream; 13 | import java.io.Reader; 14 | 15 | public class MimeFlavor { 16 | 17 | public static final MimeFlavor stringFlavor = new MimeFlavor(String.class); 18 | public static final MimeFlavor textFlavor = new MimeFlavor(Reader.class); 19 | public static final MimeFlavor domFlavor = new MimeFlavor(Document.class); 20 | public static final MimeFlavor jsonFlavor = new MimeFlavor(String.class); // TODO Is there a better type (application/json) 21 | public static final MimeFlavor byteFlavor = new MimeFlavor(InputStream.class); 22 | public static final MimeFlavor fileFlavor = new MimeFlavor(File.class); 23 | 24 | 25 | private Class cls; 26 | 27 | public MimeFlavor(Class cls) 28 | { 29 | this.cls = Objects.notNull(cls, "cls"); 30 | } 31 | 32 | public Class getRepresentation() 33 | { 34 | return cls; 35 | } 36 | 37 | 38 | public boolean equals(Object o) 39 | { 40 | if(o instanceof MimeFlavor) { 41 | MimeFlavor f = (MimeFlavor) o; 42 | return cls.equals(f); 43 | } 44 | return false; 45 | } 46 | 47 | public int hashCode() 48 | { 49 | return cls.hashCode(); 50 | } 51 | 52 | public String toString() 53 | { 54 | return "MimeFlavor<" + cls.getSimpleName() + ">"; 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/xpertss/mime/Parameter.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright XpertSoftware All rights reserved. 3 | * 4 | * Date: 3/15/11 1:15 PM 5 | */ 6 | package xpertss.mime; 7 | 8 | /** 9 | * This class holds a MIME parameter (attribute-value pair) as defined in RFC-2231. 10 | *

11 | * Parameters may be either simple or named. A simple parameter will have a {@code null} 12 | * name. 13 | */ 14 | public interface Parameter { 15 | 16 | 17 | /** 18 | * Returns the parameter name as a String or {@code null} if this is a simple 19 | * parameter. 20 | */ 21 | public String getName(); 22 | 23 | /** 24 | * Returns the parameter value as a String. 25 | */ 26 | public String getValue(); 27 | 28 | 29 | /** 30 | * Returns this parameter as a fully formatted string. 31 | */ 32 | public String toString(); 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/xpertss/mime/impl/ComplexHeaderParser.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright XpertSoftware All rights reserved. 3 | * 4 | * Date: 3/18/11 9:19 PM 5 | */ 6 | package xpertss.mime.impl; 7 | 8 | import xpertss.mime.Header; 9 | import xpertss.mime.HeaderTokenizer; 10 | import xpertss.mime.HeaderValue; 11 | import xpertss.mime.MalformedException; 12 | import xpertss.mime.Parameter; 13 | import xpertss.lang.Objects; 14 | import xpertss.utils.Utils; 15 | 16 | import java.util.ArrayList; 17 | import java.util.List; 18 | 19 | import static xpertss.mime.HeaderTokenizer.MIME; 20 | 21 | /** 22 | * A complex header is any header which likely contains multiple parts separated 23 | * by a comma. This may include both named and unnamed parts. 24 | *

 25 |  *    Range: npt=0.000-
 26 |  *    RTP-Info: url=rtsp://stream.foo.com/AVAIL.sdp/trackID=2,url=rtsp://stream.foo.com/AVAIL.sdp/trackID=5
 27 |  *    Public: DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, OPTIONS, ANNOUNCE, RECORD
 28 |  *    If-Match: "etag1", "etag2", "etag3"
 29 |  *    Accept: text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c
 30 |  * 
31 | */ 32 | public class ComplexHeaderParser extends ParameterizedHeaderParser { 33 | 34 | private final Header.Type type; 35 | private final String name; 36 | 37 | protected ComplexHeaderParser(String name, Header.Type type) 38 | { 39 | this.name = Objects.notNull(name, "name"); 40 | this.type = Objects.notNull(type, "type"); 41 | } 42 | 43 | 44 | @Override 45 | protected Header doParse(CharSequence raw) 46 | throws MalformedException 47 | { 48 | HeaderTokenizer h = new HeaderTokenizer(raw, MIME); 49 | List values = new ArrayList<>(); 50 | List parts = new ArrayList<>(); 51 | StringBuilder buf = new StringBuilder(); 52 | boolean complete = false; 53 | 54 | // First break them up into individual value pairs 55 | while(!complete) { 56 | HeaderTokenizer.Token token = h.next(); 57 | switch(token.getType()) { 58 | case HeaderTokenizer.Token.EOF: 59 | parts.add(Utils.trimAndClear(buf)); 60 | values.add(create(parts)); 61 | complete = true; 62 | break; 63 | case HeaderTokenizer.Token.LWS: 64 | buf.append(" "); // collapse whitespace 65 | continue; 66 | case ';': 67 | parts.add(Utils.trimAndClear(buf)); 68 | continue; 69 | case ',': 70 | parts.add(Utils.trimAndClear(buf)); 71 | values.add(create(parts)); 72 | continue; 73 | case HeaderTokenizer.Token.QUOTEDSTRING: 74 | buf.append('"').append(token.getValue()).append('"'); 75 | continue; 76 | case HeaderTokenizer.Token.COMMENT: 77 | buf.append('(').append(token.getValue()).append(')'); 78 | continue; 79 | default: 80 | buf.append(token.getValue()); 81 | } 82 | } 83 | return new ComplexValueHeader(name, type, values.toArray(new HeaderValue[values.size()])); 84 | } 85 | 86 | private HeaderValue create(List parts) throws MalformedException 87 | { 88 | String[] valueParts = parts.remove(0).split("=", 2); 89 | if(valueParts.length == 1) return new SimpleHeaderValue(valueParts[0], createParams(parts)); 90 | return new NamedHeaderValue(valueParts[0], valueParts[1], createParams(parts)); 91 | } 92 | 93 | 94 | private Parameter[] createParams(List parts) throws MalformedException 95 | { 96 | Parameter[] params = new Parameter[parts.size()]; 97 | String part = null; int i = 0; 98 | while(!parts.isEmpty() && (part = parts.remove(0)) != null) { 99 | params[i++] = doParameter(part); 100 | } 101 | return params; 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /src/main/java/xpertss/mime/impl/ComplexValueHeader.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright XpertSoftware All rights reserved. 3 | * 4 | * Date: 3/18/11 11:55 PM 5 | */ 6 | package xpertss.mime.impl; 7 | 8 | import xpertss.lang.Strings; 9 | import xpertss.mime.Header; 10 | import xpertss.mime.HeaderValue; 11 | import xpertss.lang.Objects; 12 | 13 | public class ComplexValueHeader implements Header { 14 | 15 | private final String name; 16 | private final Type type; 17 | private final HeaderValue[] values; 18 | 19 | public ComplexValueHeader(String name, Type type, HeaderValue[] values) 20 | { 21 | this.name = Objects.notNull(name, "name"); 22 | this.type = Objects.notNull(type, "type"); 23 | this.values = Objects.notNull(values, "values"); 24 | } 25 | 26 | public String getName() 27 | { 28 | return name; 29 | } 30 | 31 | public String getValue() 32 | { 33 | StringBuilder buf = new StringBuilder(); 34 | for(HeaderValue value : values) { 35 | if(buf.length() > 0) buf.append(", "); 36 | buf.append(value.toString()); 37 | } 38 | return buf.toString(); 39 | } 40 | 41 | public Type getType() 42 | { 43 | return type; 44 | } 45 | 46 | public int size() 47 | { 48 | return values.length; 49 | } 50 | 51 | public HeaderValue getValue(int index) 52 | { 53 | return values[index]; 54 | } 55 | 56 | public HeaderValue getValue(String name) 57 | { 58 | for(HeaderValue value : values) 59 | if(Strings.equalIgnoreCase(name, value.getName())) return value; 60 | return null; 61 | } 62 | 63 | public String toString() 64 | { 65 | return String.format("%s: %s", getName(), getValue()); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/xpertss/mime/impl/HttpHeaderParserProvider.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright XpertSoftware All rights reserved. 3 | * 4 | * Date: 3/18/11 1:51 PM 5 | */ 6 | package xpertss.mime.impl; 7 | 8 | import xpertss.mime.Header; 9 | import xpertss.mime.HeaderParser; 10 | import xpertss.mime.spi.HeaderParserProvider; 11 | 12 | import java.util.HashMap; 13 | import java.util.Map; 14 | 15 | /** 16 | * This implementation breaks down impl headers into two main categories plus a few 17 | * header by header implementations. The two main categories include: SimpleHeaders 18 | * which have only a single unnamed value and ComplexHeaders which have multiple named 19 | * and unnamed values. 20 | *

21 | * In addition to these two main categories which cover most of the possible impl 22 | * headers there are also a few header specific parsers for Cookies as an example 23 | * which do not conform to the standard parameter format. 24 | *

25 | * The main reason for the two main categories is that the special character , 26 | * has two different meanings depending on the header. In some cases it denotes a 27 | * boundary between values such as is the case on the Allow header. In other cases it 28 | * denotes a separation of elements within a value such as its use in all Date related 29 | * headers. Due to this difference two header implementations needed to be developed to 30 | * parse that usage differently. 31 | */ 32 | public class HttpHeaderParserProvider implements HeaderParserProvider { 33 | 34 | private Map parsers = new HashMap<>(); 35 | 36 | public HttpHeaderParserProvider() 37 | { 38 | // General Headers (Cache-Control, Connection, Date, Pragma, Trailer, Transfer-Encoding, Upgrade, Via, Warning) 39 | parsers.put("CACHE-CONTROL", new ComplexHeaderParser("Cache-Control", Header.Type.General)); 40 | parsers.put("CONNECTION", new SimpleHeaderParser("Connection", Header.Type.General)); 41 | parsers.put("DATE", new SimpleHeaderParser("Date", Header.Type.General)); 42 | parsers.put("PRAGMA", new SimpleHeaderParser("Pragma", Header.Type.General)); 43 | parsers.put("TRAILER", new ComplexHeaderParser("Trailer", Header.Type.General)); 44 | parsers.put("TRANSFER-ENCODING", new ComplexHeaderParser("Transfer-Encoding", Header.Type.General)); 45 | parsers.put("UPGRADE", new ComplexHeaderParser("Upgrade", Header.Type.General)); 46 | parsers.put("VIA", new ComplexHeaderParser("Via", Header.Type.General)); 47 | // TODO Warning 48 | 49 | 50 | // Request Headers (Accept, Accept-Charset, Accept-Encoding, Accept-Language, Authorization, Expect, From, Host, 51 | // If-Match, If-Modified-Since, If-None-Match, If-Range, If-Unmodified-Since, Max-Forwards, 52 | // Proxy-Authorization, Range, Referer, TE, User-Agent) 53 | parsers.put("ACCEPT", new ComplexHeaderParser("Accept", Header.Type.Request)); 54 | parsers.put("ACCEPT-CHARSET", new ComplexHeaderParser("Accept-Charset", Header.Type.Request)); 55 | parsers.put("ACCEPT-ENCODING", new ComplexHeaderParser("Accept-Encoding", Header.Type.Request)); 56 | parsers.put("ACCEPT-LANGUAGE", new ComplexHeaderParser("Accept-Language", Header.Type.Request)); 57 | parsers.put("EXPECT", new ComplexHeaderParser("Expect", Header.Type.Request)); 58 | parsers.put("FROM", new SimpleHeaderParser("From", Header.Type.Request)); 59 | parsers.put("HOST", new SimpleHeaderParser("Host", Header.Type.Request)); 60 | parsers.put("IF-MATCH", new ComplexHeaderParser("If-Match", Header.Type.Request)); 61 | parsers.put("IF-MODIFIED-SINCE", new SimpleHeaderParser("If-Modified-Since", Header.Type.Request)); 62 | parsers.put("IF-NONE-MATCH", new ComplexHeaderParser("If-None-Match", Header.Type.Request)); 63 | parsers.put("IF-RANGE", new SimpleHeaderParser("If-Range", Header.Type.Request)); 64 | parsers.put("IF-UNMODIFIED-SINCE", new SimpleHeaderParser("If-Unmodified-Since", Header.Type.Request)); 65 | parsers.put("MAX-FORWARDS", new SimpleHeaderParser("Max-Forwards", Header.Type.Request)); 66 | parsers.put("RANGE", new ComplexHeaderParser("Range", Header.Type.Request)); 67 | parsers.put("REFERER", new SimpleHeaderParser("Referer", Header.Type.Request)); 68 | parsers.put("TE", new ComplexHeaderParser("TE", Header.Type.Request)); 69 | parsers.put("USER-AGENT", new SimpleHeaderParser("User-Agent", Header.Type.Request)); 70 | 71 | parsers.put("AUTHORIZATION", new SimpleHeaderParser("Authorization", Header.Type.Request)); // kind of unique 72 | parsers.put("PROXY-AUTHORIZATION", new SimpleHeaderParser("Proxy-Authorization", Header.Type.Request)); // kind of unique 73 | // TODO Cookie 74 | 75 | 76 | // Response Headers (Accept-Ranges, Age, ETag, Location, Proxy-Authenticate, Retry-After, Server, Vary, 77 | // WWW-Authenticate) 78 | parsers.put("ACCEPT-RANGES", new ComplexHeaderParser("Accept-Ranges", Header.Type.Response)); 79 | parsers.put("AGE", new SimpleHeaderParser("Age", Header.Type.Response)); 80 | parsers.put("ETAG", new SimpleHeaderParser("ETag", Header.Type.Response)); 81 | parsers.put("LOCATION", new SimpleHeaderParser("Location", Header.Type.Response)); 82 | parsers.put("RETRY-AFTER", new SimpleHeaderParser("Retry-After", Header.Type.Response)); 83 | parsers.put("SERVER", new SimpleHeaderParser("Server", Header.Type.Response)); 84 | parsers.put("VARY", new ComplexHeaderParser("Vary", Header.Type.Response)); 85 | 86 | parsers.put("PROXY-AUTHENTICATE", new SimpleHeaderParser("Proxy-Authenticate", Header.Type.Response)); // kind of unique 87 | parsers.put("WWW-AUTHENTICATE", new SimpleHeaderParser("WWW-Authenticate", Header.Type.Response)); // kind of unique 88 | // TODO Set-Cookie and possibly Set-Cookie2 89 | 90 | 91 | // Entity Headers (Allow, Content-Encoding, Content-Length, Content-Location, Content-MD5, Content-Range, 92 | // Content-Type, Expires, Last-Modified) 93 | parsers.put("ALLOW", new ComplexHeaderParser("Allow", Header.Type.Entity)); 94 | parsers.put("CONTENT-ENCODING", new ComplexHeaderParser("Content-Encoding", Header.Type.Entity)); 95 | parsers.put("CONTENT-LANGUAGE", new ComplexHeaderParser("Content-Language", Header.Type.Entity)); 96 | parsers.put("CONTENT-LENGTH", new SimpleHeaderParser("Content-Length", Header.Type.Entity)); 97 | parsers.put("CONTENT-LOCATION", new SimpleHeaderParser("Content-Location", Header.Type.Entity)); 98 | parsers.put("CONTENT-MD5", new SimpleHeaderParser("Content-MD5", Header.Type.Entity)); 99 | parsers.put("CONTENT-RANGE", new SimpleHeaderParser("Content-Range", Header.Type.Entity)); 100 | parsers.put("CONTENT-TYPE", new SimpleHeaderParser("Content-Type", Header.Type.Entity)); 101 | parsers.put("EXPIRES", new SimpleHeaderParser("Expires", Header.Type.Entity)); 102 | parsers.put("LAST-MODIFIED", new SimpleHeaderParser("Last-Modified", Header.Type.Entity)); 103 | 104 | } 105 | 106 | public HeaderParser create(String name) 107 | { 108 | return parsers.get(name.toUpperCase()); 109 | } 110 | 111 | 112 | 113 | } 114 | -------------------------------------------------------------------------------- /src/main/java/xpertss/mime/impl/MailHeaderParserProvider.java: -------------------------------------------------------------------------------- 1 | package xpertss.mime.impl; 2 | 3 | import xpertss.mime.Header; 4 | import xpertss.mime.HeaderParser; 5 | import xpertss.mime.spi.HeaderParserProvider; 6 | 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | 10 | /** 11 | * 12 | */ 13 | public class MailHeaderParserProvider implements HeaderParserProvider { 14 | 15 | private Map parsers = new HashMap<>(); 16 | 17 | public MailHeaderParserProvider() 18 | { 19 | // rfc822 20 | parsers.put("RESENT-MESSAGE-ID", new SimpleHeaderParser("Resent-Message-ID", Header.Type.General)); 21 | parsers.put("IN-REPLY-TO", new ComplexHeaderParser("In-Reply-To", Header.Type.General)); 22 | parsers.put("MESSAGE-ID", new SimpleHeaderParser("Message-ID", Header.Type.General)); 23 | parsers.put("REFERENCES", new ComplexHeaderParser("References", Header.Type.General)); 24 | 25 | parsers.put("RETURN-PATH", new SimpleHeaderParser("Return-Path", Header.Type.General)); 26 | parsers.put("RECEIVED", new SimpleHeaderParser("Received", Header.Type.General)); 27 | parsers.put("RESENT-DATE", new SimpleHeaderParser("Resent-Date", Header.Type.General)); 28 | 29 | parsers.put("RESENT-SENDER", new SimpleHeaderParser("Resent-Sender", Header.Type.General)); 30 | parsers.put("RESENT-FROM", new SimpleHeaderParser("Resent-From", Header.Type.General)); 31 | parsers.put("REPLY-TO", new SimpleHeaderParser("Reply-To", Header.Type.General)); 32 | parsers.put("SENDER", new SimpleHeaderParser("Sender", Header.Type.General)); 33 | 34 | 35 | parsers.put("TO", new ComplexHeaderParser("To", Header.Type.General)); 36 | parsers.put("CC", new ComplexHeaderParser("Cc", Header.Type.General)); 37 | 38 | parsers.put("SUBJECT", new SimpleHeaderParser("Subject", Header.Type.General)); 39 | parsers.put("COMMENTS", new SimpleHeaderParser("Comments", Header.Type.General)); 40 | parsers.put("KEYWORDS", new ComplexHeaderParser("Keywords", Header.Type.General)); 41 | 42 | 43 | 44 | // Resent-Sender 45 | // Resent-To 46 | // Resent-Reply-To 47 | 48 | 49 | 50 | 51 | // rfc2045 52 | parsers.put("MIME-VERSION", new SimpleHeaderParser("MIME-Version", Header.Type.General)); 53 | parsers.put("CONTENT-ID", new SimpleHeaderParser("Content-ID", Header.Type.General)); 54 | parsers.put("CONTENT-TRANSFER-ENCODING", new SimpleHeaderParser("Content-Transfer-Encoding", Header.Type.General)); 55 | parsers.put("CONTENT-DESCRIPTION", new SimpleHeaderParser("Content-Description", Header.Type.General)); 56 | parsers.put("CONTENT-DISPOSITION", new SimpleHeaderParser("Content-Disposition", Header.Type.General)); 57 | parsers.put("CONTENT-DURATION", new SimpleHeaderParser("Content-Duration", Header.Type.General)); 58 | 59 | 60 | 61 | 62 | // TODO Add Mail Headers 63 | // https://tools.ietf.org/html/rfc2045 64 | // https://tools.ietf.org/html/rfc2046 65 | // https://tools.ietf.org/html/rfc2047 66 | // https://tools.ietf.org/html/rfc1123 67 | // https://tools.ietf.org/html/rfc822 68 | } 69 | 70 | @Override 71 | public HeaderParser create(String name) 72 | { 73 | return parsers.get(name.toUpperCase()); 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/xpertss/mime/impl/NamedHeaderValue.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright XpertSoftware All rights reserved. 3 | * 4 | * Date: 3/18/11 9:45 PM 5 | */ 6 | package xpertss.mime.impl; 7 | 8 | import xpertss.lang.Strings; 9 | import xpertss.mime.HeaderValue; 10 | import xpertss.mime.Parameter; 11 | import xpertss.lang.Objects; 12 | 13 | public class NamedHeaderValue implements HeaderValue { 14 | 15 | private final String name; 16 | private final String value; 17 | private final Parameter[] params; 18 | 19 | public NamedHeaderValue(String name, String value, Parameter[] params) 20 | { 21 | this.name = Objects.notNull(name, "name"); 22 | this.value = Objects.notNull(value, "value"); 23 | this.params = Objects.notNull(params, "params"); 24 | } 25 | 26 | public String getName() 27 | { 28 | return name; 29 | } 30 | 31 | public String getValue() 32 | { 33 | if(value.indexOf('"') == 0 && value.lastIndexOf('"') == value.length() - 1) { 34 | return value.substring(1, value.length() - 1); 35 | } 36 | return value; 37 | } 38 | 39 | public int size() 40 | { 41 | return params.length; 42 | } 43 | 44 | public Parameter getParameter(int index) 45 | { 46 | return params[index]; 47 | } 48 | 49 | public Parameter getParameter(String name) 50 | { 51 | for(Parameter param : params) { 52 | if(Strings.equalIgnoreCase(name, param.getName())) return param; 53 | } 54 | return null; 55 | } 56 | 57 | public String toString() 58 | { 59 | StringBuilder buf = new StringBuilder(getName()).append("=").append(value); 60 | for(Parameter param : params) { 61 | buf.append("; ").append(param.toString()); 62 | } 63 | return buf.toString(); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/xpertss/mime/impl/ParameterizedHeaderParser.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright XpertSoftware All rights reserved. 3 | * 4 | * Date: 3/19/11 8:34 AM 5 | */ 6 | package xpertss.mime.impl; 7 | 8 | import xpertss.mime.HeaderParser; 9 | import xpertss.mime.HeaderTokenizer; 10 | import xpertss.mime.MalformedException; 11 | import xpertss.mime.Parameter; 12 | import xpertss.utils.Utils; 13 | 14 | public abstract class ParameterizedHeaderParser extends HeaderParser { 15 | 16 | protected Parameter doParameter(String rawParam) throws MalformedException 17 | { 18 | HeaderTokenizer h = new HeaderTokenizer(rawParam, HeaderTokenizer.MIME); 19 | StringBuilder buf = new StringBuilder(); 20 | String name = null, value = null; 21 | boolean quoted = false; 22 | while(true) { 23 | HeaderTokenizer.Token token = h.next(); 24 | switch(token.getType()) { 25 | case HeaderTokenizer.Token.EOF: 26 | value = Utils.trimAndClear(buf); 27 | return (quoted) ? new QuotedParameter(name, value) : new SimpleParameter(name, value); 28 | case HeaderTokenizer.Token.LWS: 29 | buf.append(" "); // collapse whitespace 30 | continue; 31 | case '=': 32 | if(name != null) throw new MalformedException("malformed parameter"); 33 | name = Utils.trimAndClear(buf); 34 | continue; 35 | case HeaderTokenizer.Token.QUOTEDSTRING: 36 | quoted = true; 37 | default: 38 | buf.append(token.getValue()); 39 | } 40 | } 41 | } 42 | 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/xpertss/mime/impl/QuotedParameter.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright XpertSoftware All rights reserved. 3 | * 4 | * Date: 3/19/11 12:31 AM 5 | */ 6 | package xpertss.mime.impl; 7 | 8 | import xpertss.mime.Parameter; 9 | import xpertss.lang.Objects; 10 | 11 | // TODO Must a quoted parameter be a name/value pair or could it be simply a value?? 12 | public class QuotedParameter implements Parameter { 13 | 14 | private final String name; 15 | private final String value; 16 | 17 | public QuotedParameter(String name, String value) 18 | { 19 | this.name = Objects.notNull(name, "name"); 20 | this.value = Objects.notNull(value, "value"); 21 | } 22 | 23 | 24 | public String getName() 25 | { 26 | return name; 27 | } 28 | 29 | public String getValue() 30 | { 31 | return value; 32 | } 33 | 34 | public String toString() 35 | { 36 | return name + "=\"" + value + "\""; 37 | } 38 | 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/xpertss/mime/impl/RtspHeaderParserProvider.java: -------------------------------------------------------------------------------- 1 | package xpertss.mime.impl; 2 | 3 | import xpertss.mime.Header; 4 | import xpertss.mime.HeaderParser; 5 | import xpertss.mime.spi.HeaderParserProvider; 6 | 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | 10 | /** 11 | * A collection of headers found in the Rtsp specification. 12 | */ 13 | public class RtspHeaderParserProvider implements HeaderParserProvider { 14 | 15 | private Map parsers = new HashMap(); 16 | 17 | // RTSP Headers not already defined in HTTP 18 | public RtspHeaderParserProvider() 19 | { 20 | parsers.put("BANDWIDTH", new SimpleHeaderParser("Bandwidth", Header.Type.Request)); 21 | parsers.put("BLOCKSIZE", new SimpleHeaderParser("Blocksize", Header.Type.Request)); 22 | parsers.put("CONFERENCE", new SimpleHeaderParser("Conference", Header.Type.Request)); 23 | parsers.put("CONTENT-BASE", new SimpleHeaderParser("Content-Base", Header.Type.Entity)); 24 | parsers.put("CSEQ", new SimpleHeaderParser("CSeq", Header.Type.General)); 25 | parsers.put("PROXY-REQUIRE", new SimpleHeaderParser("Proxy-Require", Header.Type.Request)); 26 | parsers.put("PUBLIC", new ComplexHeaderParser("Public", Header.Type.Response)); 27 | parsers.put("REQUIRE", new SimpleHeaderParser("Require", Header.Type.Request)); 28 | parsers.put("RTP-INFO", new ComplexHeaderParser("RTP-Info", Header.Type.Response)); 29 | parsers.put("SCALE", new SimpleHeaderParser("Scale", Header.Type.General)); 30 | parsers.put("SPEED", new SimpleHeaderParser("Speed", Header.Type.General)); 31 | parsers.put("SESSION", new SimpleHeaderParser("Session", Header.Type.General)); 32 | parsers.put("TIMESTAMP", new SimpleHeaderParser("Timestamp", Header.Type.General)); 33 | parsers.put("TRANSPORT", new ComplexHeaderParser("Transport", Header.Type.General)); 34 | parsers.put("UNSUPPORTED", new SimpleHeaderParser("Unsupported", Header.Type.Response)); 35 | } 36 | 37 | public HeaderParser create(String name) 38 | { 39 | return parsers.get(name.toUpperCase()); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/xpertss/mime/impl/SimpleHeaderParser.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright XpertSoftware All rights reserved. 3 | * 4 | * Date: 3/18/11 1:52 PM 5 | */ 6 | package xpertss.mime.impl; 7 | 8 | import xpertss.mime.Header; 9 | import xpertss.mime.HeaderTokenizer; 10 | import xpertss.mime.HeaderValue; 11 | import xpertss.mime.MalformedException; 12 | import xpertss.mime.Parameter; 13 | import xpertss.lang.Objects; 14 | import xpertss.utils.Utils; 15 | 16 | import java.util.ArrayList; 17 | import java.util.List; 18 | 19 | import static xpertss.mime.HeaderTokenizer.MIME; 20 | import static xpertss.mime.HeaderTokenizer.Token; 21 | 22 | /** 23 | * A simple header parser parses simple impl headers. A simple header is one that has only 24 | * a single unnamed value. Some examples include, Date, Server, Content-Length, Expires, 25 | * Age, Content-Type, Location, Referrer, etc. 26 | */ 27 | public class SimpleHeaderParser extends ParameterizedHeaderParser { 28 | 29 | private final Header.Type type; 30 | private final String name; 31 | 32 | protected SimpleHeaderParser(String name, Header.Type type) 33 | { 34 | this.name = Objects.notNull(name, "name"); 35 | this.type = Objects.notNull(type, "type"); 36 | } 37 | 38 | 39 | @Override 40 | protected Header doParse(CharSequence raw) 41 | throws MalformedException 42 | { 43 | HeaderTokenizer h = new HeaderTokenizer(raw, MIME); 44 | List parts = new ArrayList(); 45 | StringBuilder buf = new StringBuilder(); 46 | boolean complete = false; 47 | 48 | while(!complete) { 49 | HeaderTokenizer.Token token = h.next(); 50 | switch(token.getType()) { 51 | case Token.EOF: 52 | parts.add(Utils.trimAndClear(buf)); 53 | complete = true; 54 | break; 55 | case Token.LWS: 56 | buf.append(" "); // collapse whitespace 57 | continue; 58 | case ';': 59 | parts.add(Utils.trimAndClear(buf)); 60 | continue; 61 | case Token.QUOTEDSTRING: 62 | buf.append('"').append(token.getValue()).append('"'); 63 | continue; 64 | case Token.COMMENT: 65 | buf.append('(').append(token.getValue()).append(')'); 66 | continue; 67 | default: 68 | buf.append(token.getValue()); 69 | } 70 | } 71 | return new SingleValueHeader(name, type, create(parts)); 72 | } 73 | 74 | 75 | private HeaderValue create(List parts) throws MalformedException 76 | { 77 | if(parts.size() == 1) return new SimpleHeaderValue(parts.get(0), new Parameter[0]); 78 | Parameter[] params = new Parameter[parts.size() - 1]; 79 | for(int i = 1; i < parts.size(); i++) params[i-1] = doParameter(parts.get(i)); 80 | return new SimpleHeaderValue(parts.get(0), params); 81 | 82 | } 83 | 84 | 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/xpertss/mime/impl/SimpleHeaderValue.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright XpertSoftware All rights reserved. 3 | * 4 | * Date: 3/18/11 9:30 PM 5 | */ 6 | package xpertss.mime.impl; 7 | 8 | import xpertss.lang.Strings; 9 | import xpertss.mime.HeaderValue; 10 | import xpertss.mime.Parameter; 11 | import xpertss.lang.Objects; 12 | 13 | public class SimpleHeaderValue implements HeaderValue { 14 | 15 | private final Parameter[] params; 16 | private final String value; 17 | 18 | public SimpleHeaderValue(String value, Parameter[] params) 19 | { 20 | this.value = Objects.notNull(value, "value"); 21 | this.params = Objects.notNull(params, "params"); 22 | } 23 | 24 | 25 | public String getName() 26 | { 27 | return null; 28 | } 29 | 30 | public String getValue() 31 | { 32 | if(value.indexOf('"') == 0 && value.lastIndexOf('"') == value.length() - 1) { 33 | return value.substring(1, value.length() - 1); 34 | } 35 | return value; 36 | } 37 | 38 | public int size() 39 | { 40 | return params.length; 41 | } 42 | 43 | public Parameter getParameter(int index) 44 | { 45 | return params[index]; 46 | } 47 | 48 | public Parameter getParameter(String name) 49 | { 50 | for(Parameter param : params) { 51 | if(Strings.equalIgnoreCase(name, param.getName())) return param; 52 | } 53 | return null; 54 | } 55 | 56 | public String toString() 57 | { 58 | StringBuilder buf = new StringBuilder(value); 59 | for(Parameter param : params) { 60 | buf.append("; ").append(param.toString()); 61 | } 62 | return buf.toString(); 63 | } 64 | 65 | 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/xpertss/mime/impl/SimpleParameter.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright XpertSoftware All rights reserved. 3 | * 4 | * Date: 3/18/11 9:42 PM 5 | */ 6 | package xpertss.mime.impl; 7 | 8 | import xpertss.lang.Objects; 9 | import xpertss.mime.Parameter; 10 | 11 | public class SimpleParameter implements Parameter { 12 | 13 | private final String name; 14 | private final String value; 15 | 16 | public SimpleParameter(String name, String value) 17 | { 18 | this.name = name; 19 | this.value = Objects.notNull(value, "value"); 20 | } 21 | 22 | 23 | public String getName() 24 | { 25 | return name; 26 | } 27 | 28 | public String getValue() 29 | { 30 | return value; 31 | } 32 | 33 | public String toString() 34 | { 35 | return (name == null) ? value : name + "=" + value; 36 | } 37 | 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/xpertss/mime/impl/SingleValueHeader.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright XpertSoftware All rights reserved. 3 | * 4 | * Date: 3/18/11 11:53 PM 5 | */ 6 | package xpertss.mime.impl; 7 | 8 | import xpertss.mime.Header; 9 | import xpertss.mime.HeaderValue; 10 | import xpertss.lang.Objects; 11 | 12 | public class SingleValueHeader implements Header { 13 | 14 | private final Type type; 15 | private final String name; 16 | private final HeaderValue[] value; 17 | 18 | public SingleValueHeader(String name, Type type, HeaderValue value) 19 | { 20 | this.name = Objects.notNull(name, "name"); 21 | this.type = Objects.notNull(type, "type"); 22 | this.value = new HeaderValue[] { value }; 23 | } 24 | 25 | public String getName() 26 | { 27 | return name; 28 | } 29 | 30 | public String getValue() 31 | { 32 | return value[0].toString(); 33 | } 34 | 35 | public Type getType() 36 | { 37 | return type; 38 | } 39 | 40 | public int size() 41 | { 42 | return 1; 43 | } 44 | 45 | public HeaderValue getValue(int index) 46 | { 47 | return value[index]; 48 | } 49 | 50 | public HeaderValue getValue(String name) 51 | { 52 | return null; 53 | } 54 | 55 | public String toString() 56 | { 57 | return String.format("%s: %s", getName(), getValue()); 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/xpertss/mime/spi/HeaderParserProvider.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright XpertSoftware All rights reserved. 3 | * 4 | * Date: 3/18/11 11:16 AM 5 | */ 6 | package xpertss.mime.spi; 7 | 8 | import xpertss.mime.HeaderParser; 9 | 10 | /** 11 | * Service provider interface defining a means to locate HeaderParser's capable 12 | * of parsing named header values. 13 | */ 14 | public interface HeaderParserProvider { 15 | 16 | /** 17 | * Return a HeaderParser implementation for the named header or {@code null} 18 | * if this provider does not support the given header. 19 | * 20 | * @param name The header name for which a parser is desired 21 | * @return A parser capable of parsing the named header or {@code null} 22 | */ 23 | public HeaderParser create(String name); 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/xpertss/net/OptionalSocketOptions.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright XpertSoftware All rights reserved. 3 | * 4 | * Date: 4/3/2015 5 | */ 6 | package xpertss.net; 7 | 8 | import java.net.SocketOption; 9 | 10 | public final class OptionalSocketOptions { 11 | 12 | /** 13 | * A socket option which gives an implementation hints as to how it would 14 | * like it to timeout read operations. This is heavily implementation 15 | * dependent. For example a protocol that uses requests and responses may 16 | * use this to timeout a request if it is not received quickly enough. 17 | */ 18 | public static final SocketOption SO_TIMEOUT = 19 | new StdSocketOption("SO_TIMEOUT", Integer.class); 20 | 21 | /** 22 | * A socket option which dictates the maximum number of pending incoming 23 | * connections to queue in the backlog before rejecting them outright. 24 | * This would in most cases only apply to an Acceptor. 25 | */ 26 | public static final SocketOption TCP_BACKLOG = 27 | new StdSocketOption("TCP_BACKLOG", Integer.class); 28 | 29 | 30 | private static class StdSocketOption implements SocketOption { 31 | private final String name; 32 | private final Class type; 33 | StdSocketOption(String name, Class type) { 34 | this.name = name; 35 | this.type = type; 36 | } 37 | @Override public String name() { return name; } 38 | @Override public Class type() { return type; } 39 | @Override public String toString() { return name; } 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/xpertss/net/SSLSocketOptions.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright XpertSoftware All rights reserved. 3 | * 4 | * Date: 4/3/2015 5 | */ 6 | package xpertss.net; 7 | 8 | import java.net.SocketOption; 9 | 10 | /** 11 | * Defines a set socket options useful for sessions that support SSL. 12 | *

13 | * The {@link SocketOption#name name} of each socket option defined by this class 14 | * is its field name. 15 | */ 16 | public final class SSLSocketOptions { 17 | 18 | private SSLSocketOptions() { } 19 | 20 | 21 | 22 | /** 23 | * Enable or disable client authentication requests. 24 | *

25 | * The value of this socket option is an SslClientAuth that dictates whether a server 26 | * socket is required to request client authentication during the handshake. 27 | *

28 | * This is only valid on SSL enabled acceptors. 29 | */ 30 | public static final SocketOption SSL_CLIENT_AUTH = 31 | new TlsSocketOption("SSL_CLIENT_AUTH", SslClientAuth.class); 32 | 33 | /** 34 | * Set the cipher suites SSL sockets will support during negotiation. 35 | *

36 | * The value of this socket option is a String[] that specifies the SSL cipher suites 37 | * the socket will accept during handshaking. This list must be supported by the 38 | * SSLContext that was configured for use. Only cipher suites in this list will be 39 | * negotiated during the handshake. Peers that do not support any of the cipher 40 | * suites in this list will not be able to establish a connection. 41 | *

42 | * This is only valid on SSL enabled connectors and acceptors. 43 | */ 44 | public static final SocketOption SSL_CIPHER_SUITES = 45 | new TlsSocketOption("SSL_CIPHER_SUITES", String[].class); 46 | 47 | /** 48 | * Set the protocols SSL sockets will support during negotiation. 49 | *

50 | * The value of this socket option is a String[] that specifies the protocols the 51 | * socket will accept. This list must be supported by the SSLContext that was 52 | * configured for use. Protocols in this case means SSLv3, TLSv1, SSLv2 as examples. 53 | * Peers that do not support any of the protocols in this list will not be able to 54 | * establish a connection. 55 | *

56 | * This is only valid on SSL enabled connectors and acceptors. 57 | */ 58 | public static final SocketOption SSL_PROTOCOLS = 59 | new TlsSocketOption("SSL_PROTOCOLS", String[].class); 60 | 61 | 62 | 63 | 64 | private static class TlsSocketOption implements java.net.SocketOption { 65 | private final String name; 66 | private final Class type; 67 | TlsSocketOption(String name, Class type) { 68 | this.name = name; 69 | this.type = type; 70 | } 71 | @Override public String name() { return name; } 72 | @Override public Class type() { return type; } 73 | @Override public String toString() { return name; } 74 | } 75 | 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/xpertss/net/SocketOptions.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright XpertSoftware All rights reserved. 3 | * 4 | * Date: 4/3/2015 5 | */ 6 | package xpertss.net; 7 | 8 | import xpertss.nio.NioSession; 9 | 10 | import java.net.SocketOption; 11 | 12 | public class SocketOptions { 13 | 14 | 15 | public static boolean set(NioSession session, SocketOption option, T value) 16 | { 17 | try { session.setOption(option, value); } catch(Exception e) { return false; } 18 | return true; 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/xpertss/net/SslClientAuth.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright XpertSoftware All rights reserved. 3 | * 4 | * Date: 2/26/11 11:25 AM 5 | */ 6 | package xpertss.net; 7 | 8 | /** 9 | * An enumeration of SSL Client Authentication options. 10 | */ 11 | public enum SslClientAuth { 12 | 13 | /** 14 | * Request client authentication but proceed if it is not provided. 15 | */ 16 | Want, 17 | 18 | /** 19 | * Request client authentication and terminate negotiation if it is not 20 | * provided. 21 | */ 22 | Need, 23 | 24 | /** 25 | * Do not request client authentication. 26 | */ 27 | None 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/xpertss/nio/AcceptHandler.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright XpertSoftware All rights reserved. 3 | * 4 | * Date: Aug 12, 2007 5 | * Time: 11:28:38 PM 6 | */ 7 | 8 | package xpertss.nio; 9 | 10 | import java.io.IOException; 11 | 12 | public interface AcceptHandler extends Selectable { 13 | 14 | public void handleAccept() throws IOException; 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/xpertss/nio/Checkable.java: -------------------------------------------------------------------------------- 1 | package xpertss.nio; 2 | 3 | import java.io.IOException; 4 | 5 | /** 6 | * A {@link Selectable} subclass which wishes to be periodically checked so 7 | * that it may validate timeouts and other state. 8 | */ 9 | public interface Checkable extends Selectable { 10 | 11 | public void handleCheckup() throws IOException; 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/xpertss/nio/ConnectHandler.java: -------------------------------------------------------------------------------- 1 | package xpertss.nio; 2 | 3 | import java.io.IOException; 4 | 5 | /** 6 | */ 7 | public interface ConnectHandler extends Selectable { 8 | 9 | public void handleConnect() throws IOException; 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/xpertss/nio/DataHandler.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright XpertSoftware All rights reserved. 3 | * 4 | * Date: Aug 12, 2007 5 | * Time: 11:33:54 PM 6 | */ 7 | 8 | package xpertss.nio; 9 | 10 | import java.io.IOException; 11 | 12 | public interface DataHandler extends Selectable { 13 | 14 | public void handleRead() throws IOException; 15 | public void handleWrite() throws IOException; 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/xpertss/nio/DeferredNioAction.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright XpertSoftware All rights reserved. 3 | * 4 | * Date: 3/26/11 2:19 PM 5 | */ 6 | package xpertss.nio; 7 | 8 | /** 9 | * An extension to NioAction who's execution will be deferred until the next 10 | * loop of the reactor thread. By default execute calls into the reactor will 11 | * execute immediately if the calling thread is the reactor thread. This can 12 | * sometimes be undesirable as you want some other action to occur first. By 13 | * implementing the DeferredNioAction interface the action will be enqueued 14 | * for the next loop allowing all other operations to proceed in the meantime. 15 | */ 16 | public interface DeferredNioAction extends NioAction { 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/xpertss/nio/NioAction.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright XpertSoftware All rights reserved. 3 | * 4 | * Date: Aug 12, 2007 5 | * Time: 11:19:27 PM 6 | */ 7 | 8 | package xpertss.nio; 9 | 10 | import java.io.IOException; 11 | import java.nio.channels.Selector; 12 | 13 | public interface NioAction { 14 | 15 | public void execute(Selector selector) 16 | throws IOException; 17 | 18 | public Selectable getSelectable(); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/xpertss/nio/NioProvider.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright XpertSoftware All rights reserved. 3 | * 4 | * Date: 3/14/11 12:59 PM 5 | */ 6 | package xpertss.nio; 7 | 8 | public interface NioProvider { 9 | 10 | public Thread newThread(Runnable r); 11 | 12 | public void serviceException(Exception error); 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/xpertss/nio/NioReactor.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright XpertSoftware All rights reserved. 3 | * 4 | * Date: 3/14/11 12:53 PM 5 | */ 6 | package xpertss.nio; 7 | 8 | import xpertss.io.NIOUtils; 9 | import xpertss.lang.Objects; 10 | import xpertss.threads.Threads; 11 | 12 | import java.nio.channels.SelectionKey; 13 | import java.nio.channels.Selector; 14 | import java.util.ArrayList; 15 | import java.util.Collections; 16 | import java.util.List; 17 | import java.util.Set; 18 | import java.util.concurrent.TimeUnit; 19 | 20 | public class NioReactor implements Runnable, NioService { 21 | 22 | private final List actions = Collections.synchronizedList(new ArrayList()); 23 | private volatile Thread thread; 24 | private NioProvider provider; 25 | private Selector selector; 26 | 27 | public NioReactor(NioProvider provider) 28 | { 29 | this.provider = Objects.notNull(provider, "provider"); 30 | } 31 | 32 | 33 | public void execute(NioAction action) 34 | { 35 | synchronized(this) { 36 | if(thread != Thread.currentThread() || action instanceof DeferredNioAction) { 37 | if(thread == null) activate(); 38 | actions.add(action); 39 | selector.wakeup(); 40 | } else { 41 | executeNow(action); 42 | } 43 | } 44 | } 45 | 46 | public boolean isSelectorThread() 47 | { 48 | return Thread.currentThread() == thread; 49 | } 50 | 51 | 52 | 53 | /** 54 | * Returns true if this server is active, false otherwise. 55 | */ 56 | public boolean isActive() 57 | { 58 | return (thread != null); 59 | } 60 | 61 | 62 | 63 | 64 | /** 65 | * Waits for this server to shutdown. 66 | */ 67 | public void await() 68 | { 69 | Threads.join(thread); 70 | } 71 | 72 | /** 73 | * Wait for the specified amount of time for this server to shutdown. This 74 | * will return false if this returned because it timed out before the server 75 | * completely shutdown, otherwise it will return true. 76 | * 77 | * @param timeout the time to wait 78 | * @param unit the unit the timeout value is measured with 79 | * @return True if the server shutdown within the allotted time 80 | */ 81 | public boolean await(long timeout, TimeUnit unit) 82 | { 83 | Threads.join(thread, timeout, unit); 84 | return (thread == null); 85 | } 86 | 87 | 88 | 89 | 90 | 91 | private void executeNow(NioAction action) 92 | { 93 | try { 94 | action.execute(selector); 95 | } catch(Exception ex) { 96 | action.getSelectable().shutdown(ex); 97 | } 98 | } 99 | 100 | 101 | private void passivate() 102 | { 103 | synchronized(this) { 104 | thread = null; 105 | NIOUtils.close(selector); 106 | selector = null; 107 | } 108 | } 109 | 110 | private void activate() 111 | { 112 | selector = NIOUtils.openSelector(); 113 | thread = provider.newThread(this); 114 | thread.start(); 115 | } 116 | 117 | 118 | public void run() 119 | { 120 | do { 121 | try { 122 | 123 | int count = selector.select(10); 124 | 125 | // Now execute any pending NIO Actions 126 | while(actions.size() > 0) executeNow(actions.remove(0)); 127 | 128 | // Process NIO events 129 | if(count > 0) { 130 | Set selectedKeys = selector.selectedKeys(); 131 | for(SelectionKey sk : selectedKeys) { 132 | if(sk.attachment() instanceof Selectable) { 133 | Selectable select = (Selectable) sk.attachment(); 134 | try { 135 | if(select instanceof AcceptHandler) { 136 | AcceptHandler handler = (AcceptHandler) select; 137 | if(sk.isValid() && sk.isAcceptable()) handler.handleAccept(); 138 | } else if(select instanceof DataHandler) { 139 | DataHandler handler = (DataHandler) select; 140 | if(sk.isValid() && sk.isReadable()) handler.handleRead(); 141 | if(sk.isValid() && sk.isWritable()) handler.handleWrite(); 142 | if(select instanceof ConnectHandler) { 143 | ConnectHandler connectable = (ConnectHandler) select; 144 | if(sk.isValid() && sk.isConnectable()) connectable.handleConnect(); 145 | } 146 | } else { 147 | assert false : select.getClass().getName(); 148 | } 149 | } catch(Exception ex) { 150 | select.shutdown(ex); 151 | } 152 | } else { 153 | assert sk.attachment() != null : "selection key with no attachment"; 154 | assert false : sk.attachment().getClass().getName(); 155 | } 156 | } 157 | selectedKeys.clear(); 158 | } 159 | 160 | 161 | // Now check the status of all user sockets 162 | Set keys = selector.keys(); 163 | for(SelectionKey key : keys) { 164 | if(key.isValid() && key.attachment() instanceof Checkable) { 165 | Checkable session = (Checkable) key.attachment(); 166 | try { session.handleCheckup(); } catch(Exception ex) { session.shutdown(ex); } 167 | } 168 | } 169 | 170 | } catch(Exception e) { 171 | provider.serviceException(e); 172 | } 173 | } while(!selector.keys().isEmpty() || !actions.isEmpty()); 174 | passivate(); 175 | } 176 | 177 | 178 | } 179 | -------------------------------------------------------------------------------- /src/main/java/xpertss/nio/NioReader.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright XpertSoftware All rights reserved. 3 | * 4 | * Date: 3/23/11 9:23 AM 5 | */ 6 | package xpertss.nio; 7 | 8 | import java.io.IOException; 9 | import java.nio.ByteBuffer; 10 | 11 | public interface NioReader { 12 | 13 | public boolean readFrom(ByteBuffer src) throws IOException; 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/xpertss/nio/NioService.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright XpertSoftware All rights reserved. 3 | * 4 | * Date: 3/14/11 11:24 AM 5 | */ 6 | package xpertss.nio; 7 | 8 | public interface NioService { 9 | 10 | public void execute(NioAction action); 11 | 12 | public boolean isSelectorThread(); 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/xpertss/nio/NioSession.java: -------------------------------------------------------------------------------- 1 | package xpertss.nio; 2 | 3 | import java.io.IOException; 4 | import java.net.SocketAddress; 5 | import java.net.SocketOption; 6 | import java.util.Set; 7 | 8 | /** 9 | */ 10 | public interface NioSession { 11 | 12 | 13 | 14 | 15 | 16 | /** 17 | * Get the current ready status of this session. 18 | */ 19 | public ReadyState getReadyState(); 20 | 21 | 22 | 23 | 24 | /** 25 | * Returns the socket address of the remote peer. 26 | * 27 | * @return The socket address of the remote peer. 28 | */ 29 | public SocketAddress getRemoteAddress(); 30 | 31 | /** 32 | * Returns the socket address of local machine which is associated with this 33 | * session. 34 | * 35 | * @return The local socket address for this session. 36 | */ 37 | public SocketAddress getLocalAddress(); 38 | 39 | /** 40 | * This will return the listen address that established this IOSession if it 41 | * is a server side session. Otherwise, if it is a client side session this 42 | * will return the same value as {@link #getRemoteAddress()}. 43 | * 44 | * @return The service address associated with this session. 45 | */ 46 | public SocketAddress getServiceAddress(); 47 | 48 | 49 | 50 | 51 | 52 | 53 | /** 54 | * Sets the value of a socket option. 55 | * 56 | * @param name The socket option 57 | * @param value The value of the socket option. A value of {@code null} 58 | * may be a valid value for some socket options. 59 | * @return This session 60 | * 61 | * @throws UnsupportedOperationException If the socket option is not 62 | * supported by this session 63 | * @throws IllegalArgumentException If the value is not a valid value 64 | * for this socket option 65 | * @throws IOException If an I/O error occurs 66 | * @see java.net.StandardSocketOptions 67 | * @see xpertss.net.OptionalSocketOptions 68 | * @see xpertss.net.SSLSocketOptions 69 | */ 70 | NioSession setOption(SocketOption name, T value) throws IOException; 71 | 72 | /** 73 | * Returns the value of a socket option. 74 | * 75 | * @param name The socket option 76 | * @return The value of the socket option. A value of {@code null} may 77 | * be a valid value for some socket options. 78 | * @throws UnsupportedOperationException If the socket option is not 79 | * supported by this channel 80 | * @throws IOException If an I/O error occurs 81 | * @see java.net.StandardSocketOptions 82 | * @see xpertss.net.OptionalSocketOptions 83 | * @see xpertss.net.SSLSocketOptions 84 | */ 85 | T getOption(SocketOption name) throws IOException; 86 | 87 | /** 88 | * Returns a set of the socket options supported by this channel. 89 | *

90 | * This method will continue to return the set of options even after 91 | * the session has been closed. 92 | * 93 | * @return A set of the socket options supported by this session 94 | */ 95 | Set> supportedOptions(); 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | /** 106 | * Returns the time in millis when this session was created. The time is 107 | * always measured as the number of milliseconds since midnight, January 108 | * 1, 1970 UTC. 109 | * 110 | * @return The time in millis when this session was created. 111 | */ 112 | public long getCreationTime(); 113 | 114 | /** 115 | * Returns the time in millis when the last I/O operation occurred. The 116 | * time is always measured as the number of milliseconds since midnight, 117 | * January 1, 1970 UTC. 118 | * 119 | * @return The time in millis when the last I/O operation occured. 120 | */ 121 | public long getLastIoTime(); 122 | 123 | /** 124 | * Returns the time in millis when the last read operation occurred. The 125 | * time is always measured as the number of milliseconds since midnight, 126 | * January 1, 1970 UTC. 127 | * 128 | * @return The time in millis when the last read operation occurred 129 | */ 130 | public long getLastReadTime(); 131 | 132 | /** 133 | * Returns the time in millis when the lst write operation occurred. The 134 | * time is always measured as the number of milliseconds since midnight, 135 | * January 1, 1970 UTC. 136 | * 137 | * @return The time in millis when the lst write operation occurred 138 | */ 139 | public long getLastWriteTime(); 140 | 141 | 142 | 143 | 144 | 145 | /** 146 | * Return the number of bytes written to this session. 147 | */ 148 | public long getBytesWritten(); 149 | 150 | /** 151 | * Return the number of bytes read from this session. 152 | */ 153 | public long getBytesRead(); 154 | 155 | 156 | 157 | 158 | 159 | /** 160 | * Returns the value of user-defined attribute of this session. 161 | * 162 | * @param key the key of the attribute 163 | * @return null if there is no attribute with the specified key 164 | */ 165 | public Object getAttribute(String key); 166 | 167 | /** 168 | * Sets a user-defined attribute. 169 | * 170 | * @param key the key of the attribute 171 | * @param value the value of the attribute 172 | */ 173 | public void setAttribute(String key, Object value); 174 | 175 | /** 176 | * Removes a user-defined attribute with the specified key. 177 | * 178 | * @param key The key identifying the attribute to remove 179 | */ 180 | public void removeAttribute(String key); 181 | 182 | /** 183 | * Returns true if this session contains the attribute with 184 | * the specified key. 185 | * 186 | * @param key The key identifying the attribute to check existance of 187 | * @return true if the named attribute exists in this session 188 | */ 189 | public boolean hasAttribute(String key); 190 | 191 | /** 192 | * Returns the set of keys of all user-defined attributes. 193 | * 194 | * @return A set of attribute keys currently defined 195 | */ 196 | public Set getAttributeKeys(); 197 | 198 | 199 | 200 | 201 | } 202 | -------------------------------------------------------------------------------- /src/main/java/xpertss/nio/NioStats.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright XpertSoftware All rights reserved. 3 | * 4 | * Date: 3/23/11 1:39 PM 5 | */ 6 | package xpertss.nio; 7 | 8 | /** 9 | */ 10 | public class NioStats { 11 | 12 | private long time = System.currentTimeMillis(); 13 | private long count; 14 | 15 | public void record(long amount) 16 | { 17 | if(amount > 0) { 18 | time = System.currentTimeMillis(); 19 | count += amount; 20 | } 21 | } 22 | 23 | public long getCount() 24 | { 25 | return count; 26 | } 27 | 28 | public long getTime() 29 | { 30 | return time; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/xpertss/nio/NioWriter.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright XpertSoftware All rights reserved. 3 | * 4 | * Date: 3/23/11 9:24 AM 5 | */ 6 | package xpertss.nio; 7 | 8 | import java.io.IOException; 9 | import java.nio.ByteBuffer; 10 | 11 | public interface NioWriter { 12 | 13 | public boolean writeTo(ByteBuffer dst) throws IOException; 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/xpertss/nio/ReadyState.java: -------------------------------------------------------------------------------- 1 | package xpertss.nio; 2 | 3 | /** 4 | * 5 | */ 6 | public enum ReadyState { 7 | 8 | Open, 9 | 10 | Connecting, 11 | 12 | Connected, 13 | 14 | Closing, 15 | 16 | Closed 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/xpertss/nio/Selectable.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright XpertSoftware All rights reserved. 3 | * 4 | * Date: Aug 12, 2007 5 | * Time: 11:17:40 PM 6 | */ 7 | 8 | package xpertss.nio; 9 | 10 | import java.nio.channels.SelectableChannel; 11 | 12 | public interface Selectable { 13 | 14 | public SelectableChannel getChannel(); 15 | 16 | public void shutdown(Exception ex); 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/xpertss/rtsp/RtspClient.java: -------------------------------------------------------------------------------- 1 | package xpertss.rtsp; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import xpertss.lang.Objects; 6 | import xpertss.nio.NioProvider; 7 | import xpertss.nio.NioReactor; 8 | import xpertss.threads.Threads; 9 | import xpertss.util.Version; 10 | import xpertss.utils.UserAgent; 11 | 12 | import java.net.URI; 13 | import java.util.concurrent.ThreadFactory; 14 | import java.util.concurrent.TimeUnit; 15 | 16 | /** 17 | */ 18 | public class RtspClient { 19 | 20 | 21 | private Logger log = LoggerFactory.getLogger(getClass()); 22 | 23 | // Product name OS/version Java/version 24 | private final NioReactor reactor; 25 | private ThreadFactory factory; 26 | private UserAgent userAgent; 27 | 28 | 29 | public RtspClient() 30 | { 31 | this.reactor = new NioReactor(new RtspNioProvider()); 32 | } 33 | 34 | 35 | 36 | 37 | 38 | 39 | /** 40 | * Call this to have a configured (and attached to the reactor service) 41 | * session. It will not be connected initially. 42 | * 43 | * @throws NullPointerException if uri is {@code null} 44 | * @throws IllegalArgumentException if the uri is invalid 45 | */ 46 | public RtspSession open(RtspHandler handler, URI uri) 47 | { 48 | if(!Objects.notNull(uri).getScheme().equals("rtsp")) 49 | throw new IllegalArgumentException("only supports rtsp urls"); 50 | return new RtspSession(reactor, handler, uri, getUserAgent()); 51 | } 52 | 53 | 54 | 55 | 56 | 57 | 58 | /** 59 | * Set the UserAgent string this client will submit to servers with each 60 | * request. 61 | */ 62 | public void setUserAgent(UserAgent userAgent) 63 | { 64 | this.userAgent = Objects.notNull(userAgent, "userAgent"); 65 | } 66 | 67 | /** 68 | * Get the UserAgent string this client is submitting to servers with each 69 | * request. 70 | */ 71 | public UserAgent getUserAgent() 72 | { 73 | return (userAgent == null) ? UserAgent.create("Aries", new Version(1,0)) : userAgent; 74 | } 75 | 76 | 77 | 78 | /** 79 | * Set the thread factory this HttpServer will use to obtain threads from. 80 | *

81 | * It is advised that the thread factory should create daemon threads with 82 | * a priority slightly above NORM_PRIORITY. 83 | * 84 | * @param factory The thread factory this server should obtain threads from 85 | * @throws IllegalStateException if this is called on an active server 86 | */ 87 | public void setThreadFactory(ThreadFactory factory) 88 | { 89 | if(isActive()) throw new IllegalStateException("server is active"); 90 | this.factory = factory; 91 | } 92 | 93 | /** 94 | * Returns the thread factory this HttpServer will obtain threads from. 95 | */ 96 | public ThreadFactory getThreadFactory() 97 | { 98 | return factory; 99 | } 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | /** 108 | * Returns true if this server is active, false otherwise. 109 | */ 110 | public boolean isActive() 111 | { 112 | return reactor.isActive(); 113 | } 114 | 115 | 116 | /** 117 | * Waits for this server to shutdown. 118 | */ 119 | public void await() 120 | { 121 | reactor.await(); 122 | } 123 | 124 | /** 125 | * Wait for the specified amount of time for this server to shutdown. This 126 | * will return false if this returned because it timed out before the server 127 | * completely shutdown, otherwise it will return true. 128 | * 129 | * @param timeout the time to wait 130 | * @param unit the unit the timeout value is measured with 131 | * @return True if the server shutdown within the allotted time 132 | */ 133 | public boolean await(long timeout, TimeUnit unit) 134 | { 135 | return reactor.await(timeout, unit); 136 | } 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | private class RtspNioProvider implements NioProvider { 147 | 148 | public Thread newThread(Runnable r) 149 | { 150 | ThreadFactory factory = getThreadFactory(); 151 | if(factory == null) { 152 | factory = Threads.newThreadFactory("RtspReactor", Thread.NORM_PRIORITY + 1, true); 153 | } 154 | return factory.newThread(r); 155 | } 156 | 157 | public void serviceException(Exception error) 158 | { 159 | log.warn("NIO Error reported", error); 160 | } 161 | } 162 | 163 | } 164 | -------------------------------------------------------------------------------- /src/main/java/xpertss/rtsp/RtspException.java: -------------------------------------------------------------------------------- 1 | package xpertss.rtsp; 2 | 3 | import java.net.ProtocolException; 4 | 5 | /** 6 | * 7 | */ 8 | public class RtspException extends ProtocolException { 9 | 10 | public enum Type { 11 | Url, Network, Source, Carrier, Audio, Video, Authentication 12 | } 13 | 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/xpertss/rtsp/RtspHandler.java: -------------------------------------------------------------------------------- 1 | package xpertss.rtsp; 2 | 3 | import java.io.IOException; 4 | import java.nio.ByteBuffer; 5 | 6 | /** 7 | * A handler receives events associated with the session. It is responsible 8 | * for coordinating the connection, the RTSP handshake, and processing or 9 | * forwarding data packets. 10 | */ 11 | public interface RtspHandler { 12 | 13 | /** 14 | * Called to indicate the connection sequence has completed. 15 | */ 16 | public void onConnect(RtspSession session); 17 | 18 | /** 19 | * Called to indicate the disconnection sequence has completed. 20 | */ 21 | public void onClose(RtspSession session); 22 | 23 | /** 24 | * Called to indicate a failure on the session and to indicate 25 | * its closure. 26 | */ 27 | public void onFailure(RtspSession session, Exception e); 28 | 29 | 30 | /** 31 | * Called to indicate data read from an interleaved channel. 32 | */ 33 | public void onData(RtspSession session, int channel, ByteBuffer data) throws IOException; 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/xpertss/rtsp/RtspMethod.java: -------------------------------------------------------------------------------- 1 | package xpertss.rtsp; 2 | 3 | import java.net.URI; 4 | 5 | /** 6 | */ 7 | public enum RtspMethod { 8 | 9 | /** 10 | * The OPTIONS method represents a request for information about the 11 | * communication options available on the request/response chain 12 | * identified by the Request-URI. This method allows the client to 13 | * determine the options and/or requirements associated with a resource, 14 | * or the capabilities of a server, without implying a resource action 15 | * or initiating a resource retrieval. 16 | */ 17 | Options(false), 18 | 19 | /** 20 | * When sent from client to server, ANNOUNCE posts the description of a 21 | * presentation or media object identified by the request URL to a 22 | * server. When sent from server to client, ANNOUNCE updates the session 23 | * description in real-time. 24 | */ 25 | Announce(true) { 26 | @Override public RtspRequest createRequest(URI uri) 27 | { 28 | throw new UnsupportedOperationException("announce is not yet supported"); 29 | } 30 | }, 31 | 32 | /** 33 | * The DESCRIBE method retrieves the description of a presentation or 34 | * media object identified by the request URL from a server. It may use 35 | * the Accept header to specify the description formats that the client 36 | * understands. The server responds with a description of the requested 37 | * resource. The DESCRIBE reply-response pair constitutes the media 38 | * initialization phase of RTSP. 39 | */ 40 | Describe(false), 41 | 42 | /** 43 | * The SETUP request for a URI specifies the transport mechanism to be 44 | * used for the streamed media. 45 | */ 46 | Setup(false), 47 | 48 | /** 49 | * The PLAY method tells the server to start sending data via the 50 | * mechanism specified in SETUP. A client MUST NOT issue a PLAY request 51 | * until any outstanding SETUP requests have been acknowledged as 52 | * successful. 53 | */ 54 | Play(false), 55 | 56 | /** 57 | * This method initiates recording a range of media data according to 58 | * the presentation description. 59 | */ 60 | Record(false) { 61 | @Override public RtspRequest createRequest(URI uri) 62 | { 63 | throw new UnsupportedOperationException("record not yet supported"); 64 | } 65 | }, 66 | 67 | /** 68 | * The PAUSE request causes the stream delivery to be interrupted 69 | * (halted) temporarily. If the request URL names a stream, only 70 | * playback and recording of that stream is halted. For example, for 71 | * audio, this is equivalent to muting. If the request URL names a 72 | * presentation or group of streams, delivery of all currently active 73 | * streams within the presentation or group is halted. 74 | */ 75 | Pause(false), 76 | 77 | /** 78 | * The TEARDOWN request stops the stream delivery for the given URI, 79 | * freeing the resources associated with it. If the URI is the 80 | * presentation URI for this presentation, any RTSP session identifier 81 | * associated with the session is no longer valid. Unless all transport 82 | * parameters are defined by the session description, a SETUP request 83 | * has to be issued before the session can be played again. 84 | */ 85 | Teardown(false); 86 | 87 | 88 | 89 | private boolean supportsEntity; 90 | 91 | private RtspMethod(boolean supportsEntity) 92 | { 93 | this.supportsEntity = supportsEntity; 94 | } 95 | 96 | 97 | /** 98 | * Create a request using the current request method to the default uri 99 | * specified when the session was opened. 100 | */ 101 | public RtspRequest createRequest() 102 | { 103 | return createRequest(null); 104 | } 105 | 106 | /** 107 | * Create a request using the current request method to the specified uri. 108 | */ 109 | public RtspRequest createRequest(URI uri) 110 | { 111 | return new RtspRequest(this, uri); 112 | } 113 | 114 | /** 115 | * Allows an entity to be sent to the server. 116 | */ 117 | public boolean supportsEntity() { return supportsEntity; } 118 | 119 | 120 | } 121 | -------------------------------------------------------------------------------- /src/main/java/xpertss/rtsp/RtspPlayer.java: -------------------------------------------------------------------------------- 1 | package xpertss.rtsp; 2 | 3 | import xpertss.SourceException; 4 | import xpertss.lang.Numbers; 5 | import xpertss.lang.Objects; 6 | import xpertss.lang.Range; 7 | import xpertss.lang.Strings; 8 | import xpertss.media.MediaChannel; 9 | import xpertss.media.MediaConsumer; 10 | import xpertss.mime.Header; 11 | import xpertss.mime.HeaderValue; 12 | import xpertss.mime.Headers; 13 | import xpertss.net.SocketOptions; 14 | import xpertss.sdp.MediaDescription; 15 | import xpertss.sdp.SessionDescription; 16 | import xpertss.sdp.SessionParser; 17 | import xpertss.utils.SafeProxy; 18 | import xpertss.utils.Utils; 19 | 20 | import java.io.IOException; 21 | import java.net.ProtocolException; 22 | import java.net.URI; 23 | import java.nio.ByteBuffer; 24 | import java.util.TreeSet; 25 | 26 | import static xpertss.net.OptionalSocketOptions.SO_TIMEOUT; 27 | import static xpertss.rtsp.RtspMethod.Describe; 28 | import static xpertss.rtsp.RtspMethod.Pause; 29 | import static xpertss.rtsp.RtspMethod.Play; 30 | import static xpertss.rtsp.RtspMethod.Setup; 31 | import static xpertss.rtsp.RtspMethod.Teardown; 32 | import static xpertss.rtsp.RtspState.*; 33 | import static xpertss.rtsp.RtspStatus.*; 34 | 35 | public class RtspPlayer { 36 | 37 | private final TreeSet channels = new TreeSet<>(); 38 | private final MediaConsumer consumer; 39 | private final RtspClient client; 40 | 41 | private volatile RtspState state = Stopped; 42 | private RtspSession session; 43 | private int connectTimeout; 44 | private int readTimeout; 45 | private URI base; 46 | 47 | 48 | 49 | public RtspPlayer(RtspClient client, MediaConsumer consumer) 50 | { 51 | this.client = Objects.notNull(client); 52 | this.consumer = SafeProxy.newInstance(MediaConsumer.class, 53 | Objects.notNull(consumer)); 54 | } 55 | 56 | 57 | 58 | 59 | public void setConnectTimeout(int connectTimeout) 60 | { 61 | this.connectTimeout = Numbers.gte(0, connectTimeout, "connectTimeout must not be negative"); 62 | } 63 | 64 | public int getConnectTimeout() 65 | { 66 | return connectTimeout; 67 | } 68 | 69 | 70 | 71 | 72 | public void setReadTimeout(int readTimeout) 73 | { 74 | this.readTimeout = Numbers.gte(0, readTimeout, "readTimeout must not be negative"); 75 | } 76 | 77 | public int getReadTimeout() 78 | { 79 | return readTimeout; 80 | } 81 | 82 | 83 | 84 | 85 | 86 | public SessionDescription getSessionDescription() 87 | { 88 | return getSessionDescription(session); 89 | } 90 | 91 | 92 | 93 | 94 | public RtspState getState() 95 | { 96 | return state; 97 | } 98 | 99 | 100 | 101 | 102 | /** 103 | * Connect to the remote media server and setup the media streams. 104 | */ 105 | public void start(URI uri) 106 | { 107 | if(state == Stopped) { 108 | state = Activating; 109 | session = client.open(new RtspPlaybackHandler(), uri); 110 | 111 | SocketOptions.set(session, SO_TIMEOUT, readTimeout); 112 | 113 | session.connect(connectTimeout); 114 | } 115 | } 116 | 117 | /** 118 | * Play the configured media streams. 119 | */ 120 | public void play() 121 | { 122 | if(state == Paused) { 123 | state = Activating; 124 | RtspRequest request = Play.createRequest(base); 125 | Headers headers = request.getHeaders(); 126 | setSessionId(session, headers); 127 | headers.setHeader("Range", "npt=0.000-"); 128 | session.execute(request, new DefaultResponseHandler() { 129 | @Override public void onOkResponse(RtspSession session, RtspResponse response) throws IOException { 130 | state = Active; 131 | } 132 | }); 133 | } 134 | } 135 | 136 | /** 137 | * Pause the configured media streams. 138 | */ 139 | public void pause() 140 | { 141 | if(state == Active) { 142 | state = Pausing; 143 | RtspRequest request = Pause.createRequest(base); 144 | Headers headers = request.getHeaders(); 145 | setSessionId(session, headers); 146 | session.execute(request, new DefaultResponseHandler() { 147 | @Override public void onOkResponse(RtspSession session, RtspResponse response) throws IOException { 148 | state = Paused; 149 | } 150 | }); 151 | } 152 | } 153 | 154 | /** 155 | * Stop playback, tear down the configured streams and disconnect. 156 | */ 157 | public void stop() 158 | { 159 | if(!Objects.isOneOf(state, Stopped, Stopping)) { 160 | state = Stopping; 161 | RtspRequest request = Teardown.createRequest(base); 162 | Headers headers = request.getHeaders(); 163 | setSessionId(session, headers); 164 | headers.setHeader("Connection", "close"); 165 | session.execute(request, new DefaultResponseHandler() { 166 | @Override public void onOkResponse(RtspSession session, RtspResponse response) throws IOException { 167 | consumer.destroyChannels(); 168 | session.close(); 169 | } 170 | }); 171 | session = null; 172 | } 173 | } 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | private static final String TRANSPORT = "RTP/AVP/TCP;unicast;interleaved=%d-%d"; 182 | 183 | private void setupChannel(RtspSession session) throws IOException 184 | { 185 | final MediaChannel channel = channels.pollFirst(); 186 | if(channel == null) { 187 | startPlayback(session); 188 | } else { 189 | String control = channel.getControl(); 190 | URI target = (Strings.isEmpty(control)) ? base : base.resolve(control); 191 | RtspRequest request = Setup.createRequest(target); 192 | Headers headers = request.getHeaders(); 193 | 194 | final Range channels = channel.getChannels(); 195 | headers.setHeader("Transport", String.format(TRANSPORT, 196 | channels.getLower(), 197 | channels.getUpper())); 198 | setSessionId(session, headers); 199 | 200 | session.execute(request, new DefaultResponseHandler() { 201 | @Override 202 | public void onOkResponse(RtspSession session, RtspResponse response) throws IOException { 203 | consumer.createChannel(channel); 204 | session.setAttribute("session.id", Utils.getBaseHeader(response, "Session")); 205 | setupChannel(session); 206 | } 207 | }); 208 | } 209 | } 210 | 211 | 212 | 213 | 214 | private void startPlayback(RtspSession session) throws IOException 215 | { 216 | RtspRequest request = Play.createRequest(base); 217 | Headers headers = request.getHeaders(); 218 | setSessionId(session, headers); 219 | headers.setHeader("Range", "npt=0.000-"); 220 | session.execute(request, new DefaultResponseHandler() { 221 | @Override public void onOkResponse(RtspSession session, RtspResponse response) throws IOException { 222 | state = Active; 223 | } 224 | }); 225 | } 226 | 227 | 228 | 229 | 230 | private static void setSessionId(RtspSession session, Headers headers) 231 | { 232 | String sessionId = (String) session.getAttribute("session.id"); 233 | if(!Strings.isEmpty(sessionId)) headers.setHeader("Session", sessionId); 234 | } 235 | 236 | private static SessionDescription getSessionDescription(RtspSession session) 237 | { 238 | return (session == null) ? null : (SessionDescription) 239 | session.getAttribute("session.description"); 240 | } 241 | 242 | 243 | private class RtspPlaybackHandler implements RtspHandler { 244 | 245 | @Override 246 | public void onConnect(RtspSession session) 247 | { 248 | RtspRequest request = Describe.createRequest(); 249 | Headers headers = request.getHeaders(); 250 | headers.setHeader("Accept", "application/sdp"); 251 | session.execute(request, new DefaultResponseHandler() { 252 | @Override public void onOkResponse(RtspSession session, RtspResponse response) throws IOException { 253 | Headers headers = response.getHeaders(); 254 | 255 | String baseUri = Headers.toString(headers.getHeader("Content-Base")); 256 | base = (Strings.isEmpty(baseUri)) ? session.getResource() : URI.create(baseUri); 257 | 258 | String contentType = Headers.toString(headers.getHeader("Content-Type")); 259 | if (Strings.equal("application/sdp", contentType)) { 260 | SessionParser parser = new SessionParser(); 261 | SessionDescription sessionDescription = parser.parse(response.getEntityBody()); 262 | session.setAttribute("session.description", sessionDescription); 263 | 264 | MediaDescription[] medias = consumer.select(sessionDescription); 265 | if(Objects.isEmpty(medias)) throw new ProtocolException("missing media resource"); 266 | channels.clear(); 267 | for(int i = 0; i < medias.length; i++) { 268 | channels.add(new MediaChannel(medias[i], new Range<>(i * 2, i * 2 + 1))); 269 | } 270 | 271 | setupChannel(session); 272 | } else { 273 | throw new ProtocolException("unexpected entity type received"); 274 | } 275 | } 276 | }); 277 | } 278 | 279 | @Override 280 | public void onClose(RtspSession session) 281 | { 282 | state = Stopped; 283 | } 284 | 285 | @Override 286 | public void onFailure(RtspSession session, Exception e) 287 | { 288 | state = Stopped; 289 | consumer.handle(e); 290 | } 291 | 292 | 293 | 294 | 295 | @Override 296 | public void onData(RtspSession session, int channelId, ByteBuffer data) throws IOException 297 | { 298 | consumer.consume(channelId, data); 299 | } 300 | 301 | 302 | } 303 | 304 | private static class DefaultResponseHandler implements RtspResponseHandler { 305 | 306 | @Override public void onResponse(RtspSession session, RtspResponse response) throws IOException { 307 | if(response.getStatus() != Ok) 308 | throw new SourceException(response.getStatus().getCode(), response.getStatusReason()); 309 | onOkResponse(session, response); 310 | } 311 | 312 | protected void onOkResponse(RtspSession session, RtspResponse response) throws IOException 313 | { 314 | } 315 | } 316 | 317 | } 318 | -------------------------------------------------------------------------------- /src/main/java/xpertss/rtsp/RtspRequest.java: -------------------------------------------------------------------------------- 1 | package xpertss.rtsp; 2 | 3 | import xpertss.io.Buffers; 4 | import xpertss.lang.Objects; 5 | import xpertss.lang.Strings; 6 | import xpertss.mime.Headers; 7 | import xpertss.nio.NioWriter; 8 | 9 | import java.io.IOException; 10 | import java.net.URI; 11 | import java.nio.ByteBuffer; 12 | 13 | import static xpertss.io.Charsets.*; 14 | import static xpertss.rtsp.RtspMethod.*; 15 | 16 | 17 | /** 18 | */ 19 | public class RtspRequest { 20 | 21 | private final Headers headers = new Headers(Headers.Type.Rtsp); 22 | private final RtspMethod method; 23 | private final URI target; 24 | 25 | RtspRequest(RtspMethod method, URI target) 26 | { 27 | if(!Objects.isOneOf(method, Options, Describe, Setup, Play, Pause, Teardown)) 28 | throw new UnsupportedOperationException("method not yet supported"); 29 | this.method = Objects.notNull(method, "method"); 30 | this.target = target; 31 | } 32 | 33 | 34 | 35 | 36 | public RtspMethod getMethod() 37 | { 38 | return method; 39 | } 40 | 41 | public Headers getHeaders() 42 | { 43 | return headers; 44 | } 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | NioWriter createWriter(URI uri) 53 | { 54 | // TODO Check that target and uri have same authority 55 | return new RequestWriter(uri); 56 | } 57 | 58 | private class RequestWriter implements NioWriter { 59 | 60 | private final URI uri; 61 | private ByteBuffer encoded; 62 | private RequestWriter(URI uri) 63 | { 64 | this.uri = Objects.notNull(uri); 65 | } 66 | 67 | @Override 68 | public boolean writeTo(ByteBuffer dst) throws IOException 69 | { 70 | if(encoded == null) encoded = encode(); 71 | Buffers.copyTo(encoded, dst); 72 | return !encoded.hasRemaining(); 73 | } 74 | 75 | private ByteBuffer encode() throws IOException 76 | { 77 | StringBuilder builder = new StringBuilder(); 78 | builder.append(Strings.toUpper(method.name())).append(" "); 79 | builder.append((target == null) ? uri : target); 80 | builder.append(" ").append("RTSP/1.0").append("\r\n"); 81 | builder.append(headers); 82 | builder.append("\r\n"); 83 | return US_ASCII.encode(builder.toString()); 84 | } 85 | 86 | } 87 | 88 | 89 | 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/xpertss/rtsp/RtspResponse.java: -------------------------------------------------------------------------------- 1 | package xpertss.rtsp; 2 | 3 | import xpertss.lang.Objects; 4 | import xpertss.lang.Strings; 5 | import xpertss.mime.Headers; 6 | 7 | import java.io.ByteArrayInputStream; 8 | import java.io.InputStream; 9 | import java.util.List; 10 | 11 | /** 12 | */ 13 | public class RtspResponse { 14 | 15 | private InputStream entity = new ByteArrayInputStream(new byte[0]); 16 | 17 | private final String reason; 18 | private final Headers headers; 19 | private final RtspStatus status; 20 | 21 | RtspResponse(RtspStatus status, String reason, Headers headers) 22 | { 23 | this.status = Objects.notNull(status); 24 | this.reason = Strings.notEmpty(reason, "reason mus tbe defined"); 25 | this.headers = Objects.notNull(headers); 26 | } 27 | 28 | 29 | public RtspStatus getStatus() 30 | { 31 | return status; 32 | } 33 | 34 | public String getStatusReason() { return reason; } 35 | 36 | public Headers getHeaders() { return headers; } 37 | 38 | 39 | public InputStream getEntityBody() 40 | { 41 | return entity; 42 | } 43 | 44 | public String toString() 45 | { 46 | return String.format("RTSP/1.0 %d %s", status.getCode(), reason); 47 | } 48 | 49 | 50 | 51 | RtspResponse withEntity(InputStream entity) 52 | { 53 | this.entity = Objects.ifNull(entity, this.entity); 54 | return this; 55 | } 56 | 57 | static RtspResponse create(List lines) 58 | { 59 | String[] status = lines.remove(0).split("\\s+", 3); 60 | return new RtspResponse(RtspStatus.valueOf(Integer.parseInt(status[1])), status[2], Headers.create(lines)); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/xpertss/rtsp/RtspResponseHandler.java: -------------------------------------------------------------------------------- 1 | package xpertss.rtsp; 2 | 3 | import java.io.IOException; 4 | 5 | /** 6 | */ 7 | public interface RtspResponseHandler { 8 | 9 | public void onResponse(RtspSession session, RtspResponse response) throws IOException; 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/xpertss/rtsp/RtspSession.java: -------------------------------------------------------------------------------- 1 | package xpertss.rtsp; 2 | 3 | 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import xpertss.io.Buffers; 7 | import xpertss.io.NIOUtils; 8 | import xpertss.lang.Booleans; 9 | import xpertss.lang.Integers; 10 | import xpertss.lang.Numbers; 11 | import xpertss.lang.Objects; 12 | import xpertss.lang.Strings; 13 | import xpertss.mime.Headers; 14 | import xpertss.nio.Checkable; 15 | import xpertss.nio.ConnectHandler; 16 | import xpertss.nio.DataHandler; 17 | import xpertss.nio.NioAction; 18 | import xpertss.nio.NioReader; 19 | import xpertss.nio.NioService; 20 | import xpertss.nio.NioSession; 21 | import xpertss.nio.NioStats; 22 | import xpertss.nio.NioWriter; 23 | import xpertss.nio.ReadyState; 24 | import xpertss.nio.Selectable; 25 | import xpertss.utils.UserAgent; 26 | import xpertss.utils.Utils; 27 | 28 | import java.io.EOFException; 29 | import java.io.IOException; 30 | import java.net.ConnectException; 31 | import java.net.ProtocolException; 32 | import java.net.SocketAddress; 33 | import java.net.SocketOption; 34 | import java.net.SocketTimeoutException; 35 | import java.net.URI; 36 | import java.nio.ByteBuffer; 37 | import java.nio.CharBuffer; 38 | import java.nio.channels.SelectableChannel; 39 | import java.nio.channels.SelectionKey; 40 | import java.nio.channels.Selector; 41 | import java.nio.channels.SocketChannel; 42 | import java.util.ArrayList; 43 | import java.util.Collections; 44 | import java.util.Deque; 45 | import java.util.HashSet; 46 | import java.util.List; 47 | import java.util.Queue; 48 | import java.util.Set; 49 | import java.util.concurrent.ConcurrentHashMap; 50 | import java.util.concurrent.ConcurrentLinkedDeque; 51 | import java.util.concurrent.ConcurrentLinkedQueue; 52 | import java.util.concurrent.ConcurrentMap; 53 | import java.util.concurrent.TimeUnit; 54 | 55 | import static java.nio.channels.SelectionKey.*; 56 | import static xpertss.net.OptionalSocketOptions.SO_TIMEOUT; 57 | import static xpertss.nio.ReadyState.*; 58 | import static xpertss.rtsp.RtspMethod.*; 59 | 60 | /** 61 | * This RtpSession impl is designed to support a streaming player not a 62 | * streaming producer. 63 | */ 64 | public class RtspSession implements NioSession, DataHandler, ConnectHandler, Checkable { 65 | 66 | 67 | private final ConcurrentMap attributes = new ConcurrentHashMap<>(); 68 | private final Deque readers = new ConcurrentLinkedDeque<>(); 69 | private final Queue writeQueue = new ConcurrentLinkedQueue<>(); 70 | private final ByteBuffer writeBuf = ByteBuffer.allocate(8192); 71 | private final ByteBuffer readBuf = ByteBuffer.allocate(8192); 72 | private final long createTime = System.currentTimeMillis(); 73 | private final ReadManager readManager = new ReadManager(); 74 | private final Logger log = LoggerFactory.getLogger(getClass()); 75 | private final NioStats write = new NioStats(); 76 | private final NioStats read = new NioStats(); 77 | private final SocketAddress address; 78 | private final SocketChannel channel; 79 | private final RtspHandler handler; 80 | private final NioService service; 81 | private final String userAgent; 82 | private final URI target; 83 | 84 | private volatile ReadyState readyState = Open; 85 | private Set> valid; 86 | private long timeoutConnection; 87 | private NioReader lastReader; 88 | private int readTimeout= 0; 89 | private int sequence = 0; 90 | 91 | 92 | RtspSession(NioService service, RtspHandler handler, URI target, UserAgent userAgent) 93 | { 94 | this.service = Objects.notNull(service); 95 | this.handler = Objects.notNull(handler); 96 | this.target = Objects.notNull(target); 97 | this.address = Utils.createSocketAddress(target, 554); 98 | this.channel = NIOUtils.openTcpSocket(false); 99 | this.userAgent = Objects.notNull(userAgent).toString(); 100 | } 101 | 102 | 103 | 104 | // Rtsp functions 105 | 106 | public URI getResource() 107 | { 108 | return target; 109 | } 110 | 111 | public void execute(RtspRequest request, RtspResponseHandler handler) 112 | { 113 | Headers headers = request.getHeaders(); 114 | headers.setHeader("CSeq", Integer.toString(++sequence)); 115 | headers.setIfNotSet("User-Agent", userAgent); 116 | writeQueue.offer(request.createWriter(target)); 117 | readers.offer(new ResponseReader(handler, request.getMethod())); 118 | service.execute(new RegisterAction(OP_WRITE)); 119 | } 120 | 121 | 122 | public void write(int channelId, ByteBuffer data) 123 | { 124 | Numbers.within(0, 255, channelId, "channelId outside range 0 - 255"); 125 | Numbers.within(0, 65545, data.remaining(), "data buffer is too large"); 126 | writeQueue.offer(new ChannelWriter(data, channelId)); 127 | service.execute(new RegisterAction(OP_WRITE)); 128 | } 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | // Base session functions 139 | 140 | /** 141 | * Connect this session to the target endpoint. Timeout the attempt after 142 | * the given timeout period measured in milliseconds has passed. 143 | */ 144 | public void connect(int timeout) 145 | { 146 | if(channel.isOpen()) { 147 | readyState = Connecting; 148 | this.timeoutConnection = Utils.computeTimeout(timeout); 149 | service.execute(new ConnectAction(channel, address)); 150 | } 151 | } 152 | 153 | /** 154 | * Close the session. 155 | */ 156 | public void close() 157 | { 158 | if(channel.isOpen()) { 159 | readyState = Closing; 160 | service.execute(new CloseAction()); 161 | } 162 | } 163 | 164 | 165 | 166 | 167 | 168 | @Override 169 | public ReadyState getReadyState() 170 | { 171 | return readyState; 172 | } 173 | 174 | 175 | 176 | 177 | 178 | 179 | @Override 180 | public SocketAddress getRemoteAddress() 181 | { 182 | return channel.socket().getRemoteSocketAddress(); 183 | } 184 | 185 | @Override 186 | public SocketAddress getLocalAddress() 187 | { 188 | return channel.socket().getLocalSocketAddress(); 189 | } 190 | 191 | @Override 192 | public SocketAddress getServiceAddress() 193 | { 194 | return address; 195 | } 196 | 197 | 198 | 199 | 200 | 201 | @Override 202 | public RtspSession setOption(SocketOption option, T value) throws IOException 203 | { 204 | if(option == SO_TIMEOUT) { 205 | this.readTimeout = Utils.maxIfZero(Numbers.gte(0, (Integer) value, "timeout must not be negative")); 206 | } else if(channel.supportedOptions().contains(option)) { 207 | channel.setOption(option, value); 208 | } 209 | return this; 210 | } 211 | 212 | 213 | @Override 214 | public T getOption(SocketOption option) throws IOException 215 | { 216 | if(option == SO_TIMEOUT) { 217 | return option.type().cast(this.readTimeout); 218 | } else if(channel.supportedOptions().contains(option)) { 219 | return channel.getOption(option); 220 | } 221 | throw new UnsupportedOperationException(); 222 | } 223 | 224 | 225 | @Override 226 | public Set> supportedOptions() 227 | { 228 | if(valid == null) { 229 | Set> set = new HashSet<>(); 230 | set.addAll(channel.supportedOptions()); 231 | set.add(SO_TIMEOUT); 232 | valid = Collections.unmodifiableSet(set); 233 | } 234 | return valid; 235 | } 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | @Override 249 | public long getCreationTime() 250 | { 251 | return createTime; 252 | } 253 | 254 | @Override 255 | public long getLastIoTime() 256 | { 257 | return Math.max(read.getTime(), write.getTime()); 258 | } 259 | 260 | @Override 261 | public long getLastReadTime() 262 | { 263 | return read.getTime(); 264 | } 265 | 266 | @Override 267 | public long getLastWriteTime() 268 | { 269 | return write.getTime(); 270 | } 271 | 272 | 273 | 274 | 275 | 276 | @Override 277 | public long getBytesWritten() 278 | { 279 | return write.getCount(); 280 | } 281 | 282 | @Override 283 | public long getBytesRead() 284 | { 285 | return read.getCount(); 286 | } 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | @Override 295 | public Object getAttribute(String key) 296 | { 297 | return attributes.get(key); 298 | } 299 | 300 | @Override 301 | public void setAttribute(String key, Object value) 302 | { 303 | attributes.put(key, value); 304 | } 305 | 306 | @Override 307 | public void removeAttribute(String key) 308 | { 309 | attributes.remove(key); 310 | } 311 | 312 | @Override 313 | public boolean hasAttribute(String key) 314 | { 315 | return attributes.containsKey(key); 316 | } 317 | 318 | @Override 319 | public Set getAttributeKeys() 320 | { 321 | return Collections.unmodifiableSet(attributes.keySet()); 322 | } 323 | 324 | 325 | 326 | 327 | 328 | 329 | @Override 330 | public void handleConnect() throws IOException 331 | { 332 | channel.finishConnect(); 333 | service.execute(new UnregisterAction(OP_CONNECT)); 334 | handler.onConnect(this); 335 | readyState = Connected; 336 | service.execute(new RegisterAction(OP_READ)); 337 | } 338 | 339 | @Override 340 | public void handleRead() throws IOException 341 | { 342 | int result = channel.read(readBuf); 343 | if(result < 0) { 344 | throw new EOFException("peer closed connection"); 345 | } else { 346 | read.record(result); 347 | readBuf.flip(); 348 | while(readBuf.hasRemaining()) { 349 | if(lastReader != null) { 350 | if(lastReader.readFrom(readBuf)) { 351 | lastReader = null; 352 | } 353 | } else if(readBuf.get(readBuf.position()) == 0x24) { 354 | if(readBuf.remaining() < 4) break; 355 | lastReader = new DataReader(); 356 | if(lastReader.readFrom(readBuf)) { 357 | lastReader = null; 358 | } 359 | } else { 360 | // TODO We need to be prepared for a spontaneous RTSP Response block 361 | // while reading channelized data. This might for example be the sever 362 | // telling us it is timing out our session due to lack of keep alive. 363 | lastReader = readers.poll(); 364 | if(lastReader != null) { 365 | if (lastReader.readFrom(readBuf)) { 366 | lastReader = null; 367 | } 368 | } else { 369 | // We probably should have some sort of ResponseReader here 370 | String str = Buffers.toHexString(readBuf, readBuf.position(), Math.min(readBuf.remaining(), 10)); 371 | log.error("Unexpected data: " + str); 372 | throw new ProtocolException("unexpected read received"); 373 | } 374 | } 375 | } 376 | readBuf.compact(); 377 | } 378 | } 379 | 380 | @Override 381 | public void handleWrite() throws IOException 382 | { 383 | NioWriter writer = writeQueue.peek(); 384 | if(writer != null) { 385 | if(writer.writeTo(writeBuf)) writeQueue.remove(); 386 | } 387 | writeBuf.flip(); 388 | if(writeBuf.hasRemaining()) { 389 | write.record(channel.write(writeBuf)); 390 | } else if(writeQueue.isEmpty()) { 391 | service.execute(new UnregisterAction(SelectionKey.OP_WRITE)); 392 | } 393 | writeBuf.compact(); 394 | } 395 | 396 | 397 | @Override 398 | public void handleCheckup() throws IOException 399 | { 400 | int timeout = Utils.maxIfZero(readTimeout); 401 | if(readyState == Connecting && System.nanoTime() >= timeoutConnection) { 402 | throw new ConnectException("connection timed out"); 403 | } else if(readyState == Connected) { 404 | ResponseReader reader = readers.peek(); 405 | if(reader != null && reader.getWaitingTime() >= timeout) { 406 | throw new SocketTimeoutException("response timed out"); 407 | } else if(readManager.isReaderActive()) { 408 | if(System.currentTimeMillis() - read.getTime() >= timeout) { 409 | throw new SocketTimeoutException("read timed out"); 410 | } 411 | } 412 | } 413 | } 414 | 415 | 416 | @Override 417 | public SelectableChannel getChannel() 418 | { 419 | return channel; 420 | } 421 | 422 | @Override 423 | public void shutdown(Exception ex) 424 | { 425 | NIOUtils.close(channel); 426 | try { 427 | if (ex == null) handler.onClose(this); 428 | else handler.onFailure(this, ex); 429 | } finally { 430 | readyState = Closed; 431 | } 432 | } 433 | 434 | 435 | 436 | 437 | private class ConnectAction implements NioAction { 438 | 439 | private final SocketAddress address; 440 | private final SocketChannel socket; 441 | 442 | public ConnectAction(SocketChannel socket, SocketAddress address) 443 | { 444 | this.socket = Objects.notNull(socket, "socket"); 445 | this.address = Objects.notNull(address, "address"); 446 | } 447 | 448 | public void execute(Selector selector) throws IOException 449 | { 450 | SelectableChannel channel = getChannel(); 451 | if(channel != null && channel.isOpen()) { 452 | SelectionKey sk = channel.keyFor(selector); 453 | if(sk == null) { 454 | channel.register(selector, OP_CONNECT, RtspSession.this); 455 | socket.connect(address); 456 | } 457 | } 458 | } 459 | 460 | public Selectable getSelectable() 461 | { 462 | return RtspSession.this; 463 | } 464 | 465 | } 466 | 467 | private class CloseAction implements NioAction { 468 | 469 | public void execute(Selector selector) 470 | { 471 | SelectableChannel channel = getChannel(); 472 | if(channel != null && channel.isOpen()) { 473 | shutdown(null); 474 | } 475 | } 476 | 477 | public Selectable getSelectable() 478 | { 479 | return RtspSession.this; 480 | } 481 | 482 | } 483 | 484 | private class RegisterAction implements NioAction { 485 | 486 | private int ops; 487 | 488 | private RegisterAction(int ops) 489 | { 490 | this.ops = ops; 491 | } 492 | 493 | public void execute(Selector selector) throws IOException 494 | { 495 | SelectableChannel channel = getChannel(); 496 | if(channel != null && channel.isOpen()) { 497 | SelectionKey sk = channel.keyFor(selector); 498 | if(sk == null) { 499 | channel.register(selector, ops, RtspSession.this); 500 | } else { 501 | sk.interestOps(sk.interestOps() | ops); 502 | } 503 | } 504 | } 505 | 506 | public Selectable getSelectable() 507 | { 508 | return RtspSession.this; 509 | } 510 | } 511 | 512 | private class UnregisterAction implements NioAction { 513 | 514 | private int ops; 515 | 516 | private UnregisterAction(int ops) 517 | { 518 | this.ops = ops; 519 | } 520 | 521 | public void execute(Selector selector) 522 | { 523 | SelectableChannel channel = getChannel(); 524 | if(channel != null) { 525 | SelectionKey sk = channel.keyFor(selector); 526 | if(sk != null && sk.isValid()) { 527 | sk.interestOps(sk.interestOps() & ~ops); 528 | } 529 | } 530 | } 531 | 532 | public Selectable getSelectable() 533 | { 534 | return RtspSession.this; 535 | } 536 | 537 | } 538 | 539 | 540 | 541 | 542 | 543 | private class ChannelWriter implements NioWriter { 544 | 545 | private ByteBuffer data; 546 | private ChannelWriter(ByteBuffer src, int channelId) 547 | { 548 | data = ByteBuffer.allocate(src.remaining() + 4); 549 | data.put((byte)0x24).put((byte)channelId).putShort((short)src.remaining()); 550 | Buffers.copyTo(src, data); 551 | data.flip(); 552 | } 553 | 554 | @Override 555 | public boolean writeTo(ByteBuffer dst) throws IOException 556 | { 557 | Buffers.copyTo(data, dst); 558 | return !(data.hasRemaining()); 559 | } 560 | } 561 | 562 | 563 | 564 | private class ResponseReader implements NioReader { 565 | 566 | private final CharBuffer lineBuffer = CharBuffer.allocate(2048); // limit header/request line length to 2k 567 | private final List lines = new ArrayList<>(); 568 | private final long createTime = System.nanoTime(); 569 | private final RtspResponseHandler handler; 570 | private final RtspMethod method; 571 | 572 | private RtspResponse response; 573 | private ByteBuffer entity; 574 | 575 | public ResponseReader(RtspResponseHandler handler, RtspMethod method) 576 | { 577 | this.handler = Objects.notNull(handler); 578 | this.method = Objects.notNull(method); 579 | } 580 | 581 | public long getWaitingTime() 582 | { 583 | return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - createTime); 584 | } 585 | 586 | 587 | @Override 588 | public boolean readFrom(ByteBuffer src) throws IOException 589 | { 590 | if(response == null) response = readResponse(src); 591 | if (response != null && readEntity(src)) { 592 | if(response.getStatus() == RtspStatus.Ok) 593 | readManager.eval(method, response.getHeaders()); 594 | handler.onResponse(RtspSession.this, response); 595 | return true; 596 | } 597 | return false; 598 | } 599 | 600 | private RtspResponse readResponse(ByteBuffer src) throws IOException 601 | { 602 | while(readLine(src, lineBuffer)) { 603 | if(lineBuffer.position() == 0) { 604 | if(lines.isEmpty()) throw new IOException("premature end of response"); 605 | return RtspResponse.create(lines); 606 | } else if(lines.size() < 30) { 607 | lines.add(Utils.consume(lineBuffer, false)); 608 | } else { 609 | throw new ProtocolException("max number of lines exceeded"); 610 | } 611 | } 612 | return null; 613 | } 614 | 615 | private boolean readEntity(ByteBuffer src) throws IOException 616 | { 617 | if(entity == null) { 618 | long length = response.getHeaders().getContentLength(); 619 | if(length > 8196L) throw new ProtocolException("response entity too large"); 620 | if(length <= 0) return true; 621 | entity = ByteBuffer.allocate(Integers.safeCast(length)); 622 | } 623 | Buffers.copyTo(src, entity); 624 | if (entity.hasRemaining()) return false; 625 | response = response.withEntity(Buffers.newInputStream(entity)); 626 | return true; 627 | } 628 | 629 | 630 | private boolean readLine(ByteBuffer src, CharBuffer dst) 631 | { 632 | while(src.hasRemaining()) { 633 | char c = (char) (src.get() & 0xff); 634 | if(c == '\n') { 635 | return true; 636 | } else if(c != '\r') { 637 | dst.append(c); 638 | } 639 | } 640 | return false; 641 | } 642 | 643 | 644 | } 645 | 646 | private class DataReader implements NioReader { 647 | 648 | private ByteBuffer data; 649 | private int channelId; 650 | 651 | @Override 652 | public boolean readFrom(ByteBuffer src) throws IOException 653 | { 654 | if(data == null) { 655 | if (src.get() != 0x24) throw new ProtocolException("expected interleaved data"); 656 | channelId = src.get(); 657 | int len = src.getShort(); 658 | data = ByteBuffer.allocate(len); 659 | } 660 | Buffers.copyTo(src, data); 661 | if (data.hasRemaining()) return false; 662 | data.flip(); 663 | handler.onData(RtspSession.this, channelId, data.asReadOnlyBuffer()); 664 | return true; 665 | } 666 | } 667 | 668 | 669 | 670 | private class ReadManager { 671 | 672 | private final ConcurrentMap channels = new ConcurrentHashMap<>(); 673 | private int count; 674 | 675 | 676 | public void eval(RtspMethod method, Headers response) 677 | { 678 | if(method == Setup) { 679 | String transport = Headers.toString(response.getHeader("Transport")); 680 | if(Strings.contains(transport, "interleaved")) { 681 | String sessionId = Headers.toString(response.getHeader("Session")); 682 | if(sessionId != null) channels.put(sessionId, false); 683 | } 684 | } else if(method == Play) { 685 | String sessionId = Headers.toString(response.getHeader("Session")); 686 | if(sessionId != null && channels.replace(sessionId, false, true)) count++; 687 | } else if(method == Pause) { 688 | String sessionId = Headers.toString(response.getHeader("Session")); 689 | if(sessionId != null && channels.replace(sessionId, true, false)) count--; 690 | } else if(method == Teardown) { 691 | String sessionId = Headers.toString(response.getHeader("Session")); 692 | if(Booleans.isTrue(channels.remove(sessionId))) count--; 693 | } 694 | } 695 | 696 | public boolean isReaderActive() 697 | { 698 | return count > 0; 699 | } 700 | 701 | } 702 | 703 | } 704 | -------------------------------------------------------------------------------- /src/main/java/xpertss/rtsp/RtspState.java: -------------------------------------------------------------------------------- 1 | package xpertss.rtsp; 2 | 3 | /** 4 | * An enumeration of Rtsp States. 5 | */ 6 | public enum RtspState { 7 | 8 | /** 9 | * The player or producer is not connected. 10 | */ 11 | Stopped, 12 | 13 | /** 14 | * The player or producer are in the process of inactivating 15 | */ 16 | Pausing, 17 | 18 | /** 19 | * The player or producer is connected and setup but not 20 | * actively playing or recording media. 21 | */ 22 | Paused, 23 | 24 | /** 25 | * The player or producer are in the process of starting 26 | * playback or recording. 27 | */ 28 | Activating, 29 | 30 | /** 31 | * The player or producer are actively playing or recording 32 | * media content. 33 | */ 34 | Active, 35 | 36 | /** 37 | * The player or producer is in the process of tearing down 38 | * the session and disconnecting. 39 | */ 40 | Stopping 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/xpertss/rtsp/RtspStatus.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright XpertSoftware All rights reserved. 3 | * 4 | * Date: 3/9/11 1:37 PM 5 | */ 6 | package xpertss.rtsp; 7 | 8 | public enum RtspStatus { 9 | 10 | Continue(100, "Continue", false), 11 | 12 | Ok(200, "Ok", true), 13 | Created(201, "Created", true), 14 | LowOnStorageSpace(250, "Low on Storage Space", true), 15 | 16 | 17 | MultipleChoices(300, "Multiple Choices", true), 18 | MovedPermanently(301, "Moved Permanently", true), 19 | MovedTemporarily(302, "Moved Temporarily", true), // HTTP/1.0 20 | SeeOther(303, "See Other", true), 21 | NotModified(304, "Not Modified", false), 22 | UseProxy(305, "Use Proxy", true), 23 | 24 | 25 | BadRequest(400, "Bad Request", true), 26 | Unauthorized(401, "Unauthorized", true), 27 | PaymentRequired(402, "Payment Required", true), 28 | Forbidden(403, "Forbidden", true), 29 | NotFound(404, "Not Found", true), 30 | MethodNotAllowed(405, "Method Not Allowed", true), 31 | NotAcceptable(406, "Not Acceptable", true), 32 | ProxyAuthenticationRequired(407, "Proxy Authentication Required", true), 33 | RequestTimeout(408, "Request Timeout", true), 34 | Gone(410, "Gone", true), 35 | 36 | LengthRequired(411, "Length Required", true), 37 | PreconditionFailed(412, "Precondition Failed", true), 38 | RequestEntityTooLarge(413, "Request Entity Too Large", true), 39 | RequestUriTooLong(414, "Request-URI Too Long", true), 40 | UnsupportedMediaType(415, "Unsupported Media Type", true), 41 | 42 | ParameterNotUnderstood(451, "Parameter Not Understood", true), 43 | ConferenceNotFound(452, "Conference Not Found", true), 44 | NotEnoughBandwidth(453, "Not Enough Bandwidth", true), 45 | SessionNotFound(454, "Session Not Found", true), 46 | MethodNotValidInThisState(455, "Method Not Valid in This State", true), 47 | HeaderFieldNotValid(456, "Header Field Not Valid for Resource", true), 48 | InvalidRange(457, "Invalid Range", true), 49 | ParameterIsReadOnly(458, "Parameter Is Read-Only", true), 50 | UnsupportedTransport(461, "Unsupported Transport", true), 51 | DestinationUnreachable(462, "Destination Unreachable", true), 52 | 53 | 54 | InternalServerError(500, "Internal Server Error", true), 55 | NotImplemented(501, "Not Implemented", true), 56 | BadGateway(502, "Bad Gateway", true), 57 | ServiceUnavailable(503, "Service Unavailable", true), 58 | GatewayTimeout(504, "Gateway Timeout", true), 59 | RTSPVersionNotSupported(505, "RTSP Version Not Supported", true), 60 | OptionNotSupported(551, "Option Not Supported", true); // WebDav 61 | 62 | 63 | private int code; 64 | private String reason; 65 | private boolean allowsEntity; 66 | 67 | private RtspStatus(int code, String reason, boolean allowsEntity) 68 | { 69 | this.code = code; 70 | this.reason = reason; 71 | this.allowsEntity = allowsEntity; 72 | } 73 | 74 | public int getCode() 75 | { 76 | return code; 77 | } 78 | 79 | public String getReason() 80 | { 81 | return reason; 82 | } 83 | 84 | public boolean allowsEntity() 85 | { 86 | return allowsEntity; 87 | } 88 | 89 | @Override 90 | public String toString() 91 | { 92 | return String.format("%d %s", getCode(), getReason()); 93 | } 94 | 95 | 96 | public boolean isInformational() 97 | { 98 | return code >= 100 && code < 200; 99 | } 100 | 101 | public boolean isSuccess() 102 | { 103 | return code >= 200 && code < 300; 104 | } 105 | 106 | public boolean isRedirection() 107 | { 108 | return code >= 300 && code < 400; 109 | } 110 | 111 | public boolean isClientError() 112 | { 113 | return code >= 400 && code < 500; 114 | } 115 | 116 | public boolean isServerError() 117 | { 118 | return code >= 500; 119 | } 120 | 121 | 122 | 123 | public static RtspStatus valueOf(int code) 124 | { 125 | for(RtspStatus status : RtspStatus.values()) { 126 | if(status.getCode() == code) return status; 127 | } 128 | throw new IllegalArgumentException("Non existent status code specified: " + code); 129 | } 130 | 131 | } 132 | -------------------------------------------------------------------------------- /src/main/java/xpertss/utils/SafeProxy.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 XpertSoftware 3 | * 4 | * Created By: cfloersch 5 | * Date: 4/17/2015 6 | */ 7 | package xpertss.utils; 8 | 9 | import xpertss.threads.Threads; 10 | 11 | import java.lang.reflect.InvocationHandler; 12 | import java.lang.reflect.InvocationTargetException; 13 | import java.lang.reflect.Method; 14 | import java.lang.reflect.Proxy; 15 | 16 | /** 17 | * This class will wrap a given class and proxy calls to it catching any runtime 18 | * exceptions that may be thrown. Those exceptions will be sent to the calling 19 | * thread's uncaught exception handler and null will be returned. 20 | *

21 | * This is principally to be used with classes who's members do not return values 22 | * such as event listeners. 23 | * 24 | * @param 25 | */ 26 | public class SafeProxy implements InvocationHandler { 27 | 28 | /** 29 | * Construct an instance of the SafeDispatcher compatible with the given listener class. 30 | */ 31 | public static T newInstance(Class proxiedClass, T proxied) 32 | { 33 | return proxiedClass.cast(Proxy.newProxyInstance(proxiedClass.getClassLoader(), 34 | new Class[] { proxiedClass }, 35 | new SafeProxy(proxied))); 36 | } 37 | 38 | 39 | private final T proxied; 40 | 41 | 42 | private SafeProxy(T proxied) 43 | { 44 | this.proxied = proxied; 45 | } 46 | 47 | 48 | @Override 49 | public Object invoke(Object proxy, final Method method, final Object[] args) 50 | throws Throwable 51 | { 52 | if ("equals".equals(method.getName())) { 53 | return (args[0] == proxy); 54 | } else if("hashCode".equals(method.getName())) { 55 | return hashCode(); 56 | } else if("toString".equals(method.getName())) { 57 | return toString(); 58 | } else { 59 | try { 60 | return method.invoke(proxied, args); 61 | } catch(InvocationTargetException e) { 62 | Threads.report(e.getTargetException()); 63 | } catch(Throwable t) { 64 | Threads.report(t); 65 | } 66 | return null; 67 | } 68 | } 69 | 70 | 71 | @Override 72 | public String toString() 73 | { 74 | return "SafeProxy<" + proxied.toString() + ">"; 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/xpertss/utils/UserAgent.java: -------------------------------------------------------------------------------- 1 | package xpertss.utils; 2 | 3 | import xpertss.lang.Objects; 4 | import xpertss.lang.Strings; 5 | import xpertss.util.Config; 6 | import xpertss.util.Platform; 7 | import xpertss.util.Version; 8 | 9 | import java.util.BitSet; 10 | 11 | /** 12 | * The UserAgent class encapsulates the process creating a UserAgent string from 13 | * the underlying platform. 14 | *

15 | * A typical UserAgent string would look something like this: 16 | *

 17 |  *   MMP/1.0 (Windows/6.1; amd64; Java/1.7) XpertRTSP/1.0
 18 |  * 
19 | * In the above example the actual user agent application is identified by MMP/1.0. 20 | * The contents in the parenthesis are the OS name/version followed by the CPU 21 | * Architecture followed by the version of Java the client is running on. Finally, 22 | * the last portion represents this RTSP library and version. 23 | *

24 | * This class allows the user some control over the user agent sent to the server 25 | * with each request. For full control you may set the User-Agent header yourself 26 | * for each call or you may extend this class and override the {@link #toString} 27 | * method.. 28 | */ 29 | public class UserAgent { 30 | 31 | private final BitSet bitset = new BitSet(3); 32 | 33 | private String appName; 34 | private Version appVersion; 35 | 36 | /** 37 | * Accessible zero argument constructor for class wishing to extend and 38 | * override this implementation. 39 | */ 40 | protected UserAgent() { } 41 | 42 | private UserAgent(String appName, Version appVersion) 43 | { 44 | this.appName = Strings.notEmpty(appName, "appName must be defined"); 45 | this.appVersion = Objects.notNull(appVersion, "appVersion"); 46 | } 47 | 48 | 49 | /** 50 | * Include the name of this RTSP library and its version in the user 51 | * agent header sent to the server with each request. 52 | */ 53 | public void includeLibrary(boolean value) 54 | { 55 | bitset.set(0, !value); 56 | } 57 | 58 | /** 59 | * Returns {@code true} if the library portion is included in the user 60 | * agent string sent to the server with each request. 61 | */ 62 | public boolean isLibraryIncluded() 63 | { 64 | return !bitset.get(0); 65 | } 66 | 67 | 68 | 69 | /** 70 | * Include the version of Java this library is running on in the user 71 | * agent header sent to the server with each request. 72 | */ 73 | public void includeJava(boolean value) 74 | { 75 | bitset.set(1, !value); 76 | } 77 | 78 | /** 79 | * Returns {@code true} if the Java portion is included in the user 80 | * agent string sent to the server with each request. 81 | */ 82 | public boolean isJavaIncluded() 83 | { 84 | return !bitset.get(1); 85 | } 86 | 87 | 88 | 89 | /** 90 | * Include the type of CPU this user agent is running on in the user 91 | * agent header sent to the server with each request. 92 | */ 93 | public void includeCpu(boolean value) 94 | { 95 | bitset.set(2, !value); 96 | } 97 | 98 | /** 99 | * Returns {@code true} if the cpu portion is included in the user 100 | * agent string sent to the server with each request. 101 | */ 102 | public boolean isCpuIncluded() 103 | { 104 | return !bitset.get(2); 105 | } 106 | 107 | 108 | @Override 109 | public String toString() 110 | { 111 | StringBuilder builder = new StringBuilder(); 112 | builder.append(String.format("%s/%s", appName, appVersion)); 113 | 114 | String osName = System.getProperty("os.name").split("\\s+")[0]; 115 | Version osVersion = Platform.osVersion(); 116 | 117 | builder.append(String.format(" (%s/%s", osName, osVersion)); 118 | if(!bitset.get(2)) { 119 | String osArch = System.getProperty("os.arch"); 120 | builder.append(String.format("; %s", osArch)); 121 | } 122 | if(!bitset.get(1)) { 123 | Version vmVersion = Platform.javaVersion(); 124 | builder.append(String.format("; Java/%s", vmVersion)); 125 | } 126 | builder.append(")"); 127 | if(!bitset.get(0)) { 128 | Config config = Config.load("version.properties", false); 129 | String libVersion = config.getProperty("version"); 130 | builder.append(String.format(" XpertRTSP/%s", libVersion)); 131 | } 132 | 133 | return builder.toString(); 134 | } 135 | 136 | 137 | /** 138 | * Create a default UserAgent object with the specified application 139 | * name and version. 140 | * 141 | * @param appName The application name 142 | * @param appVersion The application version 143 | */ 144 | public static UserAgent create(String appName, Version appVersion) 145 | { 146 | return new UserAgent(appName, appVersion); 147 | } 148 | 149 | } 150 | -------------------------------------------------------------------------------- /src/main/java/xpertss/utils/Utils.java: -------------------------------------------------------------------------------- 1 | package xpertss.utils; 2 | 3 | import xpertss.lang.Integers; 4 | import xpertss.lang.Numbers; 5 | import xpertss.lang.Strings; 6 | import xpertss.mime.Header; 7 | import xpertss.mime.Headers; 8 | import xpertss.rtsp.RtspResponse; 9 | 10 | import java.net.InetSocketAddress; 11 | import java.net.SocketAddress; 12 | import java.net.URI; 13 | import java.net.URL; 14 | import java.nio.ByteBuffer; 15 | import java.nio.CharBuffer; 16 | import java.util.concurrent.TimeUnit; 17 | 18 | /** 19 | * Useful utilities 20 | */ 21 | public class Utils { 22 | 23 | /** 24 | * Creates and returns an {@link InetSocketAddress} that represents the authority 25 | * of the given {@link java.net.URI}. If the {@link java.net.URI} does not define 26 | * a port then the specified default port is used instead. 27 | * 28 | * @throws NullPointerException if uri is {@code null} 29 | * @throws IllegalArgumentException if the port is outside the range 1 - 65545 30 | */ 31 | public static SocketAddress createSocketAddress(URI uri, int defPort) 32 | { 33 | String authority = uri.getAuthority(); 34 | if(Strings.isEmpty(authority)) 35 | throw new IllegalArgumentException("uri does not define an authority"); 36 | String[] parts = authority.split(":"); 37 | int port = defPort; 38 | if(parts.length == 2) { 39 | port = Integers.parse(parts[1], defPort); 40 | Numbers.within(1, 65545, port, String.format("%d is an invalid port", port)); 41 | } 42 | return new InetSocketAddress(parts[0], port); 43 | } 44 | 45 | 46 | 47 | 48 | public static T get(T[] items, int index) 49 | { 50 | if(items == null || index < 0) return null; 51 | return (items.length > index) ? items[index] : null; 52 | } 53 | 54 | 55 | public static String trimAndClear(StringBuilder buf) 56 | { 57 | return getAndClear(buf).trim(); 58 | } 59 | 60 | public static String getAndClear(StringBuilder buf) 61 | { 62 | try { 63 | return buf.toString(); 64 | } finally { 65 | buf.setLength(0); 66 | } 67 | } 68 | 69 | 70 | public static long computeTimeout(int millis) 71 | { 72 | if(millis == 0) millis = 60000; // default timeout of 60 seconds 73 | return System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(millis); 74 | } 75 | 76 | 77 | 78 | 79 | 80 | 81 | public static String consume(CharBuffer buf, boolean trim) 82 | { 83 | buf.flip(); 84 | int offset = 0; int end = buf.limit(); 85 | if(trim) { 86 | while(offset < end && buf.get(offset) == ' ') offset++; 87 | while(end > offset && buf.get(end - 1) == ' ') end--; 88 | } 89 | char[] result = new char[end-offset]; 90 | buf.position(offset); 91 | buf.get(result).clear(); 92 | return new String(result); 93 | } 94 | 95 | 96 | public static boolean isWhiteSpace(char c) 97 | { 98 | return (c == '\t' || c == '\r' || c == '\n' || c == ' '); 99 | } 100 | 101 | 102 | public static int maxIfZero(int value) 103 | { 104 | return (value == 0) ? Integer.MAX_VALUE : value; 105 | } 106 | 107 | 108 | public static String getHeader(RtspResponse response, String name) 109 | { 110 | return Headers.toString(response.getHeaders().getHeader(name)); 111 | } 112 | 113 | public static String getBaseHeader(RtspResponse response, String name) 114 | { 115 | Header header = response.getHeaders().getHeader(name); 116 | return header.getValue(0).getValue(); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/services/xpertss.mime.spi.HeaderParserProvider: -------------------------------------------------------------------------------- 1 | xpertss.mime.impl.MailHeaderParserProvider 2 | xpertss.mime.impl.HttpHeaderParserProvider 3 | xpertss.mime.impl.RtspHeaderParserProvider -------------------------------------------------------------------------------- /src/main/resources/version.properties: -------------------------------------------------------------------------------- 1 | version: ${project.version} -------------------------------------------------------------------------------- /src/test/java/xpertss/mime/HeaderParserTest.java: -------------------------------------------------------------------------------- 1 | package xpertss.mime; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | public class HeaderParserTest { 8 | 9 | 10 | @Test 11 | public void testTransport() 12 | { 13 | Header header = HeaderParser.parse("Transport", "RTP/AVP/TCP;unicast;interleaved=0-1"); 14 | assertNotNull(header.getValue(0)); 15 | assertNotNull(header.getValue(0).getParameter("interleaved")); 16 | assertEquals("unicast", header.getValue(0).getParameter(0).getValue()); 17 | } 18 | 19 | @Test 20 | public void testComplexWithEquals() 21 | { 22 | Header header = HeaderParser.parse("RTP-Info", "url=rtsp://stream.manheim.com:999/AVAIL.sdp/trackID=2,url=rtsp://stream.manheim.com:999/AVAIL.sdp/trackID=5"); 23 | assertEquals(2, header.size()); 24 | 25 | HeaderValue valueOne = header.getValue(0); 26 | assertEquals("rtsp://stream.manheim.com:999/AVAIL.sdp/trackID=2", valueOne.getValue()); 27 | 28 | HeaderValue valueTwo = header.getValue(1); 29 | assertEquals("rtsp://stream.manheim.com:999/AVAIL.sdp/trackID=5", valueTwo.getValue()); 30 | 31 | // TODO How would I access these HeaderValues by name when they both have the same name 32 | } 33 | 34 | @Test 35 | public void testSplit() 36 | { 37 | String[] parts = "url=rtsp://stream.manheim.com:999/AVAIL.sdp/trackID=2".split("=", 2); 38 | assertEquals(2, parts.length); 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /src/test/java/xpertss/mime/HeadersTest.java: -------------------------------------------------------------------------------- 1 | package xpertss.mime; 2 | 3 | import org.junit.Test; 4 | 5 | import java.util.Enumeration; 6 | 7 | import static org.junit.Assert.*; 8 | 9 | public class HeadersTest { 10 | 11 | 12 | @Test 13 | public void testEmpty() 14 | { 15 | Headers headers = new Headers(Headers.Type.Rtsp); 16 | assertNull(headers.getHeader("Content-Type")); 17 | assertFalse(headers.headers().hasMoreElements()); 18 | } 19 | 20 | @Test 21 | public void testGetSet() 22 | { 23 | Headers headers = new Headers(Headers.Type.Rtsp); 24 | assertNull(headers.getHeader("Content-Length")); 25 | headers.setHeader("Content-Length", "200"); 26 | assertNotNull(headers.getHeader("Content-Length")); 27 | assertEquals("200", Headers.toString(headers.getHeader("Content-Length"))); 28 | assertTrue(headers.headers().hasMoreElements()); // should have an element 29 | assertTrue(headers.contains("Content-Length")); // should be found 30 | assertTrue(headers.contains("Content-length")); // case doesn't matter 31 | } 32 | 33 | @Test 34 | public void testOrdering() 35 | { 36 | Headers headers = new Headers(Headers.Type.Rtsp); 37 | headers.setHeader("Connection", "5"); 38 | headers.setHeader("CSeq", "1"); 39 | headers.setHeader("Content-Base", "3"); 40 | headers.setHeader("Vary", "4"); 41 | headers.setHeader("User-Agent", "2"); 42 | 43 | Enumeration

e = headers.headers(); 44 | assertEquals("1", Headers.toString(e.nextElement())); 45 | assertEquals("2", Headers.toString(e.nextElement())); 46 | assertEquals("3", Headers.toString(e.nextElement())); 47 | assertEquals("4", Headers.toString(e.nextElement())); 48 | assertEquals("5", Headers.toString(e.nextElement())); 49 | } 50 | 51 | @Test 52 | public void testMultipleHeaders() 53 | { 54 | Headers headers = new Headers(Headers.Type.Rtsp); 55 | headers.addHeader("Received", "from localhost"); 56 | headers.addHeader("Received", "from gateway"); 57 | 58 | Enumeration
e = headers.headers(); 59 | assertEquals("from localhost", Headers.toString(e.nextElement())); 60 | assertEquals("from gateway", Headers.toString(e.nextElement())); 61 | assertFalse(e.hasMoreElements()); 62 | 63 | assertEquals(2, headers.getHeaders("Received").length); 64 | assertEquals(2, headers.getHeaders("received").length); 65 | 66 | assertEquals(2, headers.remove("Received")); 67 | assertEquals(0, headers.remove("Received")); 68 | } 69 | 70 | @Test 71 | public void testSetIfNotSet() 72 | { 73 | Headers headers = new Headers(Headers.Type.Rtsp); 74 | headers.setIfNotSet("User-Agent", "one"); 75 | headers.setIfNotSet("User-Agent", "two"); 76 | assertEquals("one", Headers.toString(headers.getHeader("User-Agent"))); 77 | assertEquals(1, headers.getHeaders("User-Agent").length); 78 | 79 | assertEquals(1, headers.remove("User-Agent")); 80 | } 81 | 82 | @Test 83 | public void testHeaderParameters() 84 | { 85 | Headers headers = new Headers(Headers.Type.Rtsp); 86 | headers.setIfNotSet("Session", "rzRIqDVnQVDRTppy;timeout=60"); 87 | 88 | Header header = headers.getHeader("Session"); 89 | assertEquals("rzRIqDVnQVDRTppy; timeout=60", Headers.toString(header)); 90 | assertEquals("rzRIqDVnQVDRTppy", header.getValue(0).getValue()); 91 | assertEquals("60", header.getValue(0).getParameter("timeout").getValue()); 92 | } 93 | 94 | 95 | } -------------------------------------------------------------------------------- /src/test/java/xpertss/rtsp/RtspClientTest.java: -------------------------------------------------------------------------------- 1 | package xpertss.rtsp; 2 | 3 | import org.junit.Test; 4 | import xpertss.lang.Range; 5 | import xpertss.media.MediaChannel; 6 | import xpertss.media.MediaConsumer; 7 | import xpertss.media.MediaType; 8 | import xpertss.sdp.MediaDescription; 9 | import xpertss.sdp.SessionDescription; 10 | 11 | import java.net.ConnectException; 12 | import java.net.URI; 13 | import java.net.UnknownHostException; 14 | import java.nio.ByteBuffer; 15 | import java.util.HashMap; 16 | import java.util.Map; 17 | import java.util.concurrent.TimeUnit; 18 | import java.util.function.Consumer; 19 | 20 | 21 | import static org.junit.Assert.*; 22 | import static org.mockito.Matchers.any; 23 | import static org.mockito.Matchers.same; 24 | import static org.mockito.Mockito.mock; 25 | import static org.mockito.Mockito.times; 26 | import static org.mockito.Mockito.verify; 27 | import static xpertss.nio.ReadyState.Closed; 28 | import static xpertss.nio.ReadyState.Closing; 29 | import static xpertss.nio.ReadyState.Connected; 30 | import static xpertss.nio.ReadyState.Connecting; 31 | import static xpertss.nio.ReadyState.Open; 32 | 33 | public class RtspClientTest { 34 | 35 | 36 | @Test 37 | public void testClientConnectSequence() 38 | { 39 | RtspHandler handler = mock(RtspHandler.class); 40 | RtspClient client = new RtspClient(); 41 | RtspSession session = client.open(handler, URI.create("rtsp://stream.manheim.com:999/AVAIL.sdp")); 42 | 43 | assertEquals(Open, session.getReadyState()); 44 | session.connect(500); 45 | assertEquals(Connecting, session.getReadyState()); 46 | 47 | while(session.getReadyState() == Connecting); 48 | 49 | verify(handler, times(1)).onConnect(same(session)); 50 | assertEquals(Connected, session.getReadyState()); 51 | 52 | session.close(); 53 | assertEquals(Closing, session.getReadyState()); 54 | 55 | while(session.getReadyState() == Closing); 56 | assertEquals(Closed, session.getReadyState()); 57 | 58 | verify(handler, times(1)).onClose(same(session)); 59 | } 60 | 61 | 62 | @Test 63 | public void testClientConnectSequenceTimeout() 64 | { 65 | RtspHandler handler = mock(RtspHandler.class); 66 | RtspClient client = new RtspClient(); 67 | RtspSession session = client.open(handler, URI.create("rtsp://10.0.0.1:999/AVAIL.sdp")); 68 | 69 | assertEquals(Open, session.getReadyState()); 70 | session.connect(100); 71 | assertEquals(Connecting, session.getReadyState()); 72 | 73 | while(session.getReadyState() == Connecting); 74 | 75 | verify(handler, times(1)).onFailure(same(session), any(ConnectException.class)); 76 | assertEquals(Closed, session.getReadyState()); 77 | } 78 | 79 | @Test 80 | public void testClientConnectSequenceUnknownHost() 81 | { 82 | RtspHandler handler = mock(RtspHandler.class); 83 | RtspClient client = new RtspClient(); 84 | RtspSession session = client.open(handler, URI.create("rtsp://doesnot.exist.host.com:999/AVAIL.sdp")); 85 | 86 | assertEquals(Open, session.getReadyState()); 87 | session.connect(100); 88 | assertEquals(Connecting, session.getReadyState()); 89 | 90 | while(session.getReadyState() == Connecting); 91 | 92 | verify(handler, times(1)).onFailure(same(session), any(UnknownHostException.class)); 93 | assertEquals(Closed, session.getReadyState()); 94 | } 95 | 96 | 97 | 98 | 99 | 100 | @Test 101 | public void testRtspPlayer() 102 | { 103 | RtspClient client = new RtspClient(); 104 | RtspPlayer player = new RtspPlayer(client, new MediaConsumer() { 105 | 106 | private Map> channels = new HashMap<>(); 107 | 108 | @Override 109 | public MediaDescription[] select(SessionDescription sdp) 110 | { 111 | return sdp.getMediaDescriptions(); 112 | } 113 | 114 | @Override 115 | public void createChannel(MediaChannel channel) 116 | { 117 | final MediaType type = channel.getType(); 118 | Range range = channel.getChannels(); 119 | channels.put(range.getLower(), new Consumer() { 120 | long start = System.nanoTime(); 121 | @Override public void accept(ByteBuffer data) { 122 | long millis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start); 123 | int seq = (int) (data.getShort(2) & 0xffff); 124 | long ts = (long) (data.getInt(4) & 0xffffffff); 125 | System.out.println(String.format("%s: %010d {seq=%05d, ts=%d, size=%04d}", type.name(), millis, seq, ts, data.remaining())); 126 | } 127 | }); 128 | channels.put(range.getUpper(), new Consumer() { 129 | @Override 130 | public void accept(ByteBuffer byteBuffer) { 131 | System.out.println(String.format("%s - RTP Sender Report Received", type.name())); 132 | } 133 | }); 134 | } 135 | 136 | @Override 137 | public void destroyChannels() 138 | { 139 | System.out.println("destroy called"); 140 | channels.clear(); 141 | } 142 | 143 | @Override 144 | public void consume(int channelId, ByteBuffer data) 145 | { 146 | Consumer handler = channels.get(channelId); 147 | if(handler == null) System.out.println(String.format("Received packet on unknown channel %d", channelId)); 148 | else handler.accept(data); 149 | } 150 | 151 | @Override 152 | public void handle(Throwable t) 153 | { 154 | t.printStackTrace(); 155 | } 156 | 157 | 158 | }); 159 | 160 | player.setReadTimeout(5000); 161 | player.start(URI.create("rtsp://stream.manheim.com:999/AVAIL.sdp")); 162 | assertEquals(RtspState.Activating, player.getState()); 163 | client.await(3, TimeUnit.SECONDS); 164 | assertEquals(RtspState.Active, player.getState()); 165 | player.pause(); 166 | assertEquals(RtspState.Pausing, player.getState()); 167 | client.await(3, TimeUnit.SECONDS); 168 | assertEquals(RtspState.Paused, player.getState()); 169 | player.play(); 170 | assertEquals(RtspState.Activating, player.getState()); 171 | client.await(3, TimeUnit.SECONDS); 172 | assertEquals(RtspState.Active, player.getState()); 173 | player.stop(); 174 | assertEquals(RtspState.Stopping, player.getState()); 175 | client.await(); 176 | assertEquals(RtspState.Stopped, player.getState()); 177 | } 178 | 179 | 180 | 181 | } -------------------------------------------------------------------------------- /src/test/java/xpertss/rtsp/RtspMethodTest.java: -------------------------------------------------------------------------------- 1 | package xpertss.rtsp; 2 | 3 | import org.junit.Test; 4 | import xpertss.lang.Booleans; 5 | 6 | import java.util.concurrent.ConcurrentHashMap; 7 | import java.util.concurrent.ConcurrentMap; 8 | 9 | import static org.junit.Assert.*; 10 | 11 | public class RtspMethodTest { 12 | 13 | 14 | 15 | @Test 16 | public void testBooleansDefaultUnboxedValue() 17 | { 18 | ConcurrentMap map = new ConcurrentHashMap<>(); 19 | assertFalse(Booleans.isTrue(map.get("NonExistent"))); 20 | assertFalse(Booleans.isTrue(map.remove("NonExistent"))); 21 | assertFalse(map.replace("NonExistent", false, true)); 22 | //assertFalse(map.replace(null, false, true)); 23 | } 24 | 25 | } -------------------------------------------------------------------------------- /src/test/java/xpertss/rtsp/RtspResponseTest.java: -------------------------------------------------------------------------------- 1 | package xpertss.rtsp; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | public class RtspResponseTest { 8 | 9 | @Test 10 | public void testResponseSplit() 11 | { 12 | String response = "500 Internal Error"; 13 | String[] parts = response.split("\\s+", 2); 14 | assertEquals(2, parts.length); 15 | assertEquals("500", parts[0]); 16 | assertEquals("Internal Error", parts[1]); 17 | } 18 | 19 | 20 | } -------------------------------------------------------------------------------- /src/test/java/xpertss/utils/UserAgentTest.java: -------------------------------------------------------------------------------- 1 | package xpertss.utils; 2 | 3 | import org.junit.Test; 4 | import xpertss.util.Version; 5 | 6 | import static org.junit.Assert.*; 7 | 8 | public class UserAgentTest { 9 | 10 | @Test 11 | public void testUserAgent() 12 | { 13 | String userAgent = UserAgent.create("MMP", new Version(2,1)).toString(); 14 | assertTrue(userAgent.startsWith("MMP/2.1")); 15 | assertTrue(userAgent.contains("XpertRTSP/")); 16 | assertTrue(userAgent.contains(System.getProperty("os.arch"))); 17 | assertTrue(userAgent.contains("Java")); 18 | } 19 | 20 | @Test 21 | public void testUserAgentNoCpu() 22 | { 23 | UserAgent agent = UserAgent.create("MMP", new Version(2,1)); 24 | agent.includeCpu(false); 25 | String userAgent = agent.toString(); 26 | assertTrue(userAgent.startsWith("MMP/2.1")); 27 | assertTrue(userAgent.contains("XpertRTSP/")); 28 | assertFalse(userAgent.contains(System.getProperty("os.arch"))); 29 | assertTrue(userAgent.contains("Java")); 30 | } 31 | 32 | @Test 33 | public void testUserAgentNoJava() 34 | { 35 | UserAgent agent = UserAgent.create("MMP", new Version(2,1)); 36 | agent.includeJava(false); 37 | String userAgent = agent.toString(); 38 | assertTrue(userAgent.startsWith("MMP/2.1")); 39 | assertTrue(userAgent.contains("XpertRTSP/")); 40 | assertTrue(userAgent.contains(System.getProperty("os.arch"))); 41 | assertFalse(userAgent.contains("Java")); 42 | } 43 | 44 | @Test 45 | public void testUserAgentNoLibrary() 46 | { 47 | UserAgent agent = UserAgent.create("MMP", new Version(2,1)); 48 | agent.includeLibrary(false); 49 | String userAgent = agent.toString(); 50 | assertTrue(userAgent.startsWith("MMP/2.1")); 51 | assertFalse(userAgent.contains("XpertRTSP/")); 52 | assertTrue(userAgent.contains(System.getProperty("os.arch"))); 53 | assertTrue(userAgent.contains("Java")); 54 | } 55 | 56 | } -------------------------------------------------------------------------------- /src/test/java/xpertss/utils/UtilsTest.java: -------------------------------------------------------------------------------- 1 | package xpertss.utils; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | public class UtilsTest { 8 | 9 | 10 | 11 | 12 | 13 | 14 | } --------------------------------------------------------------------------------