rtsp_server 0.0.2-universal-java

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/.gemtest +0 -0
  3. data/ChangeLog.rdoc +74 -0
  4. data/Gemfile +3 -0
  5. data/LICENSE.rdoc +20 -0
  6. data/README.rdoc +152 -0
  7. data/Rakefile +23 -0
  8. data/bin/rtsp_client +133 -0
  9. data/features/client_changes_state.feature +58 -0
  10. data/features/client_requests.feature +27 -0
  11. data/features/control_streams_as_client.feature +26 -0
  12. data/features/step_definitions/client_changes_state_steps.rb +52 -0
  13. data/features/step_definitions/client_requests_steps.rb +68 -0
  14. data/features/step_definitions/control_streams_as_client_steps.rb +34 -0
  15. data/features/support/env.rb +50 -0
  16. data/features/support/hooks.rb +3 -0
  17. data/lib/ext/logger.rb +8 -0
  18. data/lib/rtsp/client.rb +520 -0
  19. data/lib/rtsp/common.rb +148 -0
  20. data/lib/rtsp/error.rb +6 -0
  21. data/lib/rtsp/global.rb +63 -0
  22. data/lib/rtsp/helpers.rb +28 -0
  23. data/lib/rtsp/message.rb +272 -0
  24. data/lib/rtsp/request.rb +39 -0
  25. data/lib/rtsp/response.rb +47 -0
  26. data/lib/rtsp/server.rb +311 -0
  27. data/lib/rtsp/socat_streaming.rb +320 -0
  28. data/lib/rtsp/stream_server.rb +37 -0
  29. data/lib/rtsp/transport_parser.rb +96 -0
  30. data/lib/rtsp/version.rb +4 -0
  31. data/lib/rtsp.rb +6 -0
  32. data/rtsp.gemspec +44 -0
  33. data/spec/rtsp/client_spec.rb +326 -0
  34. data/spec/rtsp/helpers_spec.rb +53 -0
  35. data/spec/rtsp/message_spec.rb +420 -0
  36. data/spec/rtsp/response_spec.rb +306 -0
  37. data/spec/rtsp/transport_parser_spec.rb +137 -0
  38. data/spec/rtsp_spec.rb +27 -0
  39. data/spec/spec_helper.rb +88 -0
  40. data/spec/support/fake_rtsp_server.rb +123 -0
  41. data/tasks/roodi.rake +9 -0
  42. data/tasks/roodi_config.yaml +14 -0
  43. data/tasks/stats.rake +12 -0
  44. metadata +280 -0
@@ -0,0 +1,148 @@
1
+ module RTSP
2
+
3
+ # Contains common methods belonging to Request and Response classes.
4
+ module Common
5
+
6
+ # @return [String] The unparsed request as a String.
7
+ def to_s
8
+ @raw_body
9
+ end
10
+
11
+ # Custom redefine to make sure all the dynamically created instance
12
+ # variables are displayed when this method is called.
13
+ #
14
+ # @return [String]
15
+ def inspect
16
+ me = "#<#{self.class.name}:#{self.__id__} "
17
+
18
+ self.instance_variables.each do |variable|
19
+ me << "#{variable}=#{instance_variable_get(variable).inspect}, "
20
+ end
21
+
22
+ me.sub!(/, $/, "")
23
+ me << ">"
24
+
25
+ me
26
+ end
27
+
28
+ # Takes the raw request text and splits it into a 2-element Array, where 0
29
+ # is the text containing the headers and 1 is the text containing the body.
30
+ #
31
+ # @param [String] raw_request
32
+ # @return [Array<String>] 2-element Array containing the head and body of
33
+ # the request. Body will be nil if there wasn't one in the request.
34
+ def split_head_and_body_from raw_request
35
+ head_and_body = raw_request.split("\r\n\r\n", 2)
36
+ head = head_and_body.first
37
+ body = head_and_body.last == head ? nil : head_and_body.last
38
+
39
+ [head, body]
40
+ end
41
+
42
+ # Pulls out the RTSP version, request code, and request message (AKA the
43
+ # status line info) into instance variables.
44
+ #
45
+ # @param [String] line The String containing the status line info.
46
+ def extract_status_line(line)
47
+ /RTSP\/(?<rtsp_version>\d\.\d)/ =~ line
48
+ /(?<url>rtsp:\/\/.*) RTSP/ =~ line
49
+ /rtsp:\/\/.*stream(?<stream_index>\d*)m?\/?.* RTSP/ =~ line
50
+ @url = url
51
+ @stream_index = stream_index.to_i
52
+
53
+ if rtsp_version.nil?
54
+ raise RTSP::Error, "Status line corrupted: #{line}"
55
+ end
56
+ end
57
+
58
+ # Returns the transport URL.
59
+ #
60
+ # @return [String] Transport URL associated with the request.
61
+ def transport_url
62
+ /client_port=(?<port>.*)-/ =~ transport
63
+
64
+ if port.nil?
65
+ log("Could not find client port associated with transport", :warn)
66
+ else
67
+ "#{@remote_host}:#{port}"
68
+ end
69
+ end
70
+
71
+ # Checks if the request is for a multicast stream.
72
+ #
73
+ # @return [Boolean] true if the request is for a multicast stream.
74
+ def multicast?
75
+ return false if @url.nil?
76
+
77
+ @url.end_with? "m"
78
+ end
79
+
80
+ # Reads through each header line of the RTSP request, extracts the
81
+ # request code, request message, request version, and creates a
82
+ # snake-case accessor with that value set.
83
+ #
84
+ # @param [String] head The section of headers from the request text.
85
+ def parse_head head
86
+ lines = head.split "\r\n"
87
+
88
+ lines.each_with_index do |line, i|
89
+ if i == 0
90
+ extract_status_line(line)
91
+ next
92
+ end
93
+
94
+ if line.include? "Session: "
95
+ value = {}
96
+ line =~ /Session: (\d+)/
97
+ value[:session_id] = $1.to_i
98
+
99
+ if line =~ /timeout=(.+)/
100
+ value[:timeout] = $1.to_i
101
+ end
102
+
103
+ create_reader("session", value)
104
+ elsif line.include? ": "
105
+ header_and_value = line.strip.split(":", 2)
106
+ header_name = header_and_value.first.downcase.gsub(/-/, "_")
107
+ create_reader(header_name, header_and_value[1].strip)
108
+ end
109
+ end
110
+ end
111
+
112
+ # Reads through each line of the RTSP response body and parses it if
113
+ # needed. Returns a SDP::Description if the Content-Type is
114
+ # 'application/sdp', otherwise returns the String that was passed in.
115
+ #
116
+ # @param [String] body
117
+ # @return [SDP::Description,String]
118
+ def parse_body body
119
+ if body =~ /^(\r\n|\n)/
120
+ body.gsub!(/^(\r\n|\n)/, '')
121
+ end
122
+
123
+ if @content_type == "application/sdp"
124
+ SDP.parse body
125
+ else
126
+ body
127
+ end
128
+ end
129
+
130
+ private
131
+
132
+ # Creates an attr_reader with the name given and sets it to the value
133
+ # that's given.
134
+ #
135
+ # @param [String] name
136
+ # @param [String,Hash] value
137
+ def create_reader(name, value)
138
+ unless value.empty?
139
+ if value.is_a? String
140
+ value = value =~ /^[0-9]*$/ ? value.to_i : value
141
+ end
142
+ end
143
+
144
+ instance_variable_set("@#{name}", value)
145
+ self.instance_eval "def #{name}; @#{name}; end"
146
+ end
147
+ end
148
+ end
data/lib/rtsp/error.rb ADDED
@@ -0,0 +1,6 @@
1
+ module RTSP
2
+
3
+ # Custom error for RTSP problems.
4
+ class Error < StandardError
5
+ end
6
+ end
@@ -0,0 +1,63 @@
1
+ require_relative '../ext/logger'
2
+
3
+ module RTSP
4
+ module Global
5
+ DEFAULT_RTSP_PORT = 554
6
+ DEFAULT_VERSION = '1.0'
7
+
8
+ # Sets whether to log RTSP requests & responses.
9
+ attr_writer :log
10
+
11
+ # @return [Boolean] true if logging is enabled; false if it's turned off.
12
+ def log?
13
+ @log != false
14
+ end
15
+
16
+ # Sets the type logger to use.
17
+ attr_writer :logger
18
+
19
+ # By default, this creates a standard Ruby Logger. If a different type was
20
+ # passed in via +#logger=+, this returns that object.
21
+ #
22
+ # @return [Logger]
23
+ def logger
24
+ @logger ||= ::Logger.new STDOUT
25
+ end
26
+
27
+ # @return [Symbol] The Logger method to use for logging all messages.
28
+ attr_writer :log_level
29
+
30
+ # The Logger method to use for logging all messages.
31
+ #
32
+ # @return [Symbol] Defaults to +:debug+.
33
+ def log_level
34
+ @log_level ||= :debug
35
+ end
36
+
37
+ # @param [String] message The string to log.
38
+ def log(message, level=log_level)
39
+ logger.send(level, message) if log?
40
+ end
41
+
42
+ # Use to disable the raising of +RTSP::Error+s.
43
+ attr_writer :raise_errors
44
+
45
+ # @return [Boolean] true if set to raise errors; false if not.
46
+ def raise_errors?
47
+ @raise_errors != false
48
+ end
49
+
50
+ # @return [String] The RTSP version.
51
+ def rtsp_version
52
+ @version ||= DEFAULT_VERSION
53
+ end
54
+
55
+ # Resets class variables back to defaults.
56
+ def reset_config!
57
+ self.log = true
58
+ self.logger = ::Logger.new STDOUT
59
+ self.log_level = :debug
60
+ self.raise_errors = true
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,28 @@
1
+ require 'uri'
2
+ require_relative 'global'
3
+ require_relative 'error'
4
+
5
+ module RTSP
6
+ module Helpers
7
+ include RTSP::Global
8
+
9
+ # Takes the URL given and turns it into a URI. This allows for enforcing
10
+ # values for each part of the URI.
11
+ #
12
+ # @param [String] url The URL to turn in to a URI.
13
+ # @return [URI]
14
+ def build_resource_uri_from url
15
+ if url.is_a? String
16
+ url = "rtsp://#{url}" unless url =~ /^rtsp/
17
+
18
+ resource_uri = URI.parse url
19
+ resource_uri.port ||= DEFAULT_RTSP_PORT
20
+
21
+ resource_uri
22
+
23
+ else
24
+ raise RTSP::Error, "url must be a String."
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,272 @@
1
+ require_relative 'helpers'
2
+ require_relative 'error'
3
+ require_relative 'version'
4
+
5
+ module RTSP
6
+
7
+ # This class is responsible for building a single RTSP message that can be
8
+ # used by both clients and servers.
9
+ #
10
+ # Only message types defined in {RFC 2326}[http://tools.ietf.org/html/rfc2326]
11
+ # are implemented, however if you need to add a new message type (perhaps for
12
+ # some custom server implementation?), you can simply add to the supported
13
+ # list by:
14
+ # RTSP::Message.message_types << :barrel_roll
15
+ #
16
+ # You can then build it like a standard message:
17
+ # message = RTSP::Message.barrel_roll("192.168.1.10").with_headers({
18
+ # cseq: 123, content_type: "video/x-m4v" })
19
+ class Message
20
+ include RTSP::Helpers
21
+
22
+ RTSP_ACCEPT_TYPE = "application/sdp"
23
+ RTSP_DEFAULT_NPT = "0.000-"
24
+ RTSP_DEFAULT_SEQUENCE_NUMBER = 1
25
+ USER_AGENT =
26
+ "RubyRTSP/#{RTSP::VERSION} (Ruby #{RUBY_VERSION}-p#{RUBY_PATCHLEVEL})"
27
+
28
+ @message_types = [
29
+ :announce,
30
+ :describe,
31
+ :get_parameter,
32
+ :options,
33
+ :play,
34
+ :pause,
35
+ :record,
36
+ :redirect,
37
+ :set_parameter,
38
+ :setup,
39
+ :teardown
40
+ ]
41
+
42
+ # TODO: define #describe somewhere so I can actually test that method.
43
+ class << self
44
+
45
+ # Lists the method/message types this class can create.
46
+ # @return [Array<Symbol>]
47
+ attr_accessor :message_types
48
+
49
+ # Make sure the class responds to our message types.
50
+ #
51
+ # @param [Symbol] method
52
+ def respond_to?(method)
53
+ @message_types.include?(method) || super
54
+ end
55
+
56
+ # Creates a new message based on the given method type and URI.
57
+ #
58
+ # @param [Symbol] method
59
+ # @param [Array] args
60
+ # @return [RTSP::Message]
61
+ def method_missing(method, *args)
62
+ request_uri = args.first
63
+
64
+ if @message_types.include? method
65
+ self.new(method, request_uri)
66
+ else
67
+ super
68
+ end
69
+ end
70
+ end
71
+
72
+ attr_reader :method_type
73
+ attr_reader :request_uri
74
+ attr_reader :headers
75
+ attr_reader :body
76
+ attr_writer :rtsp_version
77
+
78
+ # @param [Symbol] :method_type The RTSP method to build and send.
79
+ # @param [String] request_uri The URL to communicate to.
80
+ def initialize(method_type, request_uri)
81
+ @method_type = method_type
82
+ @request_uri = build_resource_uri_from request_uri
83
+ @headers = default_headers
84
+ @body = ""
85
+ @version = DEFAULT_VERSION
86
+ end
87
+
88
+ # Adds the header and its value to the list of headers for the message.
89
+ #
90
+ # @param [Symbol] type The header type.
91
+ # @param [] value The value to set the header field to.
92
+ def header(type, value)
93
+ if type.is_a? Symbol
94
+ headers[type] = value
95
+ else
96
+ raise RTSP::Error, "Header type must be a Symbol (i.e. :cseq)."
97
+ end
98
+ end
99
+
100
+ # Use to message-chain with one of the method types; used when creating a
101
+ # new Message to add headers you want.
102
+ #
103
+ # @example Simple header
104
+ # RTSP::Message.options("192.168.1.10").with_headers({ cseq: @cseq })
105
+ # @example Multi-word header
106
+ # RTSP::Message.options("192.168.1.10").with_headers({ user_agent:
107
+ # 'My RTSP Client 1.0' }) # => "OPTIONS 192.168.1.10 RTSP 1.0\r\n
108
+ # # CSeq: 1\r\n
109
+ # # User-Agent: My RTSP Client 1.0\r\n"
110
+ # @param [Hash] new_headers The headers to add to the Request. The Hash
111
+ # key of each will be converted from snake_case to Rtsp-Style.
112
+ # @return [RTSP::Message]
113
+ def with_headers(new_headers)
114
+ add_headers new_headers
115
+
116
+ self
117
+ end
118
+
119
+ def add_headers(new_headers)
120
+ @headers.merge! new_headers
121
+ end
122
+
123
+ # Use when creating a new Message to add body you want.
124
+ #
125
+ # @example Simple header
126
+ # RTSP::Message.options("192.168.1.10").with_body("The body!")
127
+ # @param [Hash] new_headers The headers to add to the Request. The Hash
128
+ # key will be capitalized; if
129
+ def with_body(new_body)
130
+ add_body new_body
131
+
132
+ self
133
+ end
134
+
135
+ def add_body new_body
136
+ add_headers({ content_length: new_body.length })
137
+ @body = new_body
138
+ end
139
+
140
+ # @param [String] value Content to send as the body of the message.
141
+ # Generally this will be a String of some sort, but could be binary data as
142
+ # well. Also, this adds the Content-Length header to the header list.
143
+ def body= value
144
+ add_body value
145
+ end
146
+
147
+ # @return [String] The message as a String.
148
+ def to_s
149
+ message.to_s
150
+ end
151
+
152
+ ###########################################################################
153
+ # PRIVATES
154
+ private
155
+
156
+ # Builds the request message to send to the server/client.
157
+ #
158
+ # @return [String]
159
+ def message
160
+ message = "#{@method_type.to_s.upcase} #{@request_uri} RTSP/#{@version}\r\n"
161
+ message << headers_to_s(@headers)
162
+ message << "\r\n"
163
+ message << "#{@body}" unless @body.nil?
164
+
165
+ #message.each_line { |line| RTSP::Client.log line.strip }
166
+
167
+ message
168
+ end
169
+
170
+ # Returns the required/default headers for the provided method.
171
+ #
172
+ # @return [Hash] The default headers for the given method.
173
+ def default_headers
174
+ headers = {}
175
+
176
+ headers[:cseq] ||= RTSP_DEFAULT_SEQUENCE_NUMBER
177
+ headers[:user_agent] ||= USER_AGENT
178
+
179
+ case @method_type
180
+ when :describe
181
+ headers[:accept] = RTSP_ACCEPT_TYPE
182
+ when :announce
183
+ headers[:content_type] = RTSP_ACCEPT_TYPE
184
+ when :play
185
+ headers[:range] = "npt=#{RTSP_DEFAULT_NPT}"
186
+ when :get_parameter
187
+ headers[:content_type] = 'text/parameters'
188
+ when :set_parameter
189
+ headers[:content_type] = 'text/parameters'
190
+ else
191
+ {}
192
+ end
193
+
194
+ headers
195
+ end
196
+
197
+ # Turns headers from Hash(es) into a String, where each element
198
+ # is a String in the form: [Header Type]: value(s)\r\n.
199
+ #
200
+ # @param [Hash] headers The headers to put to string.
201
+ # @return [String]
202
+ def headers_to_s headers
203
+ header_string = headers.inject("") do |result, (key, value)|
204
+ header_name = key.to_s.split(/_/).map do |header|
205
+ header.capitalize
206
+ end.join('-')
207
+
208
+ header_name = "CSeq" if header_name == "Cseq"
209
+
210
+ if value.is_a?(Hash) || value.is_a?(Array)
211
+ if header_name == "Content-Type"
212
+ values = values_to_s(value, ", ")
213
+ else
214
+ values = values_to_s(value)
215
+ end
216
+
217
+ result << "#{header_name}: #{values}\r\n"
218
+ else
219
+ result << "#{header_name}: #{value}\r\n"
220
+ end
221
+
222
+ result
223
+ end
224
+
225
+ arr = header_string.split "\r\n"
226
+ # Move the Session header to the top
227
+ session_index = arr.index { |a| a =~ /Session/ }
228
+ unless session_index.nil?
229
+ session = arr.delete_at(session_index)
230
+ arr.unshift(session)
231
+ end
232
+
233
+ # Move the User-Agent header to the top
234
+ user_agent_index = arr.index { |a| a =~ /User-Agent/ }
235
+ unless user_agent_index.nil?
236
+ user_agent = arr.delete_at(user_agent_index)
237
+ arr.unshift(user_agent)
238
+ end
239
+
240
+ # Move the CSeq header to the top
241
+ cseq_index = arr.index { |a| a =~ /CSeq/ }
242
+ cseq = arr.delete_at(cseq_index)
243
+ arr.unshift(cseq)
244
+
245
+ # Put it all back to a String
246
+ header_string = arr.join("\r\n")
247
+ header_string << "\r\n"
248
+ end
249
+
250
+ # Turns header values into a single string.
251
+ #
252
+ # @param [] values The header values to put to string.
253
+ # @param [String] separator The character to use to separate multiple
254
+ # values that define a header.
255
+ # @return [String] The header values as a single string.
256
+ def values_to_s(values, separator=";")
257
+ result = values.inject("") do |values_string, (header_field, header_field_value)|
258
+ if header_field.is_a? Symbol
259
+ values_string << "#{header_field}=#{header_field_value}"
260
+ elsif header_field.is_a? Hash
261
+ values_string << values_to_s(header_field)
262
+ else
263
+ values_string << header_field.to_s
264
+ end
265
+
266
+ values_string + separator
267
+ end
268
+
269
+ result.sub!(/#{separator}$/, '') if result.end_with? separator
270
+ end
271
+ end
272
+ end
@@ -0,0 +1,39 @@
1
+ require 'rubygems'
2
+ require 'sdp'
3
+ require_relative 'error'
4
+ require_relative 'global'
5
+ require_relative 'common'
6
+
7
+ module RTSP
8
+
9
+ # Parses raw request data from the server/client and turns it into
10
+ # attr_readers.
11
+ class Request
12
+ extend RTSP::Global
13
+ include RTSP::Common
14
+
15
+ attr_reader :rtsp_version
16
+ attr_reader :code
17
+ attr_reader :message
18
+ attr_reader :body
19
+ attr_reader :url
20
+ attr_reader :stream_index
21
+ attr_accessor :remote_host
22
+
23
+ # @param [String] raw_request The raw request string returned from the
24
+ # server/client.
25
+ # @param [String] remote_host The IP address of the remote host.
26
+ def initialize(raw_request, remote_host)
27
+ if raw_request.nil? || raw_request.empty?
28
+ raise RTSP::Error,
29
+ "#{self.class} received nil or empty string--this shouldn't happen."
30
+ end
31
+
32
+ @raw_body = raw_request
33
+ @remote_host = remote_host
34
+
35
+ head, body = split_head_and_body_from @raw_body
36
+ parse_head(head)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,47 @@
1
+ require 'rubygems'
2
+ require 'sdp'
3
+ require_relative 'error'
4
+ require_relative 'common'
5
+
6
+ module RTSP
7
+
8
+ # Parses raw response data from the server/client and turns it into
9
+ # attr_readers.
10
+ class Response
11
+ include RTSP::Common
12
+ attr_reader :rtsp_version
13
+ attr_reader :code
14
+ attr_reader :message
15
+ attr_reader :body
16
+
17
+ # @param [String] raw_response The raw response string returned from the
18
+ # server/client.
19
+ def initialize(raw_response)
20
+ if raw_response.nil? || raw_response.empty?
21
+ raise RTSP::Error,
22
+ "#{self.class} received nil string--this shouldn't happen."
23
+ end
24
+
25
+ @raw_body = raw_response
26
+
27
+ head, body = split_head_and_body_from @raw_body
28
+ parse_head(head)
29
+ @body = parse_body(body)
30
+ end
31
+
32
+ # Pulls out the RTSP version, response code, and response message (AKA the
33
+ # status line info) into instance variables.
34
+ #
35
+ # @param [String] line The String containing the status line info.
36
+ def extract_status_line(line)
37
+ line =~ /RTSP\/(\d\.\d) (\d\d\d) ([^\r\n]+)/
38
+ @rtsp_version = $1
39
+ @code = $2.to_i
40
+ @message = $3
41
+
42
+ if @rtsp_version.nil?
43
+ raise RTSP::Error, "Status line corrupted: #{line}"
44
+ end
45
+ end
46
+ end
47
+ end