rtsp 0.0.1.alpha → 0.1.0

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 (57) hide show
  1. data/.document +3 -0
  2. data/.infinity_test +1 -1
  3. data/.yardopts +4 -0
  4. data/ChangeLog.rdoc +9 -0
  5. data/Gemfile +15 -6
  6. data/Gemfile.lock +78 -40
  7. data/LICENSE.rdoc +20 -0
  8. data/PostInstall.txt +0 -3
  9. data/README.rdoc +85 -36
  10. data/Rakefile +33 -49
  11. data/bin/rtsp_client +129 -0
  12. data/features/client_changes_state.feature +58 -0
  13. data/features/client_requests.feature +27 -0
  14. data/features/control_streams_as_client.feature +26 -0
  15. data/features/step_definitions/client_changes_state_steps.rb +46 -0
  16. data/features/step_definitions/client_requests_steps.rb +74 -0
  17. data/features/step_definitions/control_streams_as_client_steps.rb +34 -0
  18. data/features/support/env.rb +31 -29
  19. data/features/support/hooks.rb +3 -0
  20. data/gemspec.yml +30 -0
  21. data/lib/ext/logger.rb +8 -0
  22. data/lib/rtsp.rb +3 -6
  23. data/lib/rtsp/capturer.rb +105 -0
  24. data/lib/rtsp/client.rb +446 -204
  25. data/lib/rtsp/error.rb +6 -0
  26. data/lib/rtsp/global.rb +63 -0
  27. data/lib/rtsp/helpers.rb +28 -0
  28. data/lib/rtsp/message.rb +270 -0
  29. data/lib/rtsp/response.rb +89 -29
  30. data/lib/rtsp/transport_parser.rb +64 -0
  31. data/lib/rtsp/version.rb +4 -0
  32. data/nsm_test.rb +26 -0
  33. data/rtsp.gemspec +284 -0
  34. data/sarix_test.rb +23 -0
  35. data/soma_test.rb +39 -0
  36. data/spec/rtsp/client_spec.rb +302 -27
  37. data/spec/rtsp/helpers_spec.rb +53 -0
  38. data/spec/rtsp/message_spec.rb +420 -0
  39. data/spec/rtsp/response_spec.rb +144 -58
  40. data/spec/rtsp/transport_parser_spec.rb +54 -0
  41. data/spec/rtsp_spec.rb +3 -3
  42. data/spec/spec_helper.rb +66 -7
  43. data/spec/support/fake_rtsp_server.rb +123 -0
  44. data/tasks/metrics.rake +27 -0
  45. data/tasks/roodi_config.yml +14 -0
  46. data/tasks/stats.rake +12 -0
  47. metadata +174 -183
  48. data/.autotest +0 -23
  49. data/History.txt +0 -4
  50. data/Manifest.txt +0 -26
  51. data/bin/rtsp +0 -121
  52. data/features/step_definitions/stream_steps.rb +0 -50
  53. data/features/stream.feature +0 -17
  54. data/features/support/common.rb +0 -1
  55. data/features/support/world.rb +0 -1
  56. data/lib/rtsp/request_messages.rb +0 -104
  57. data/lib/rtsp/status_code.rb +0 -7
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)
39
+ logger.send(log_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,270 @@
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 { |header| header.capitalize }.join('-')
205
+
206
+ header_name = "CSeq" if header_name == "Cseq"
207
+
208
+ if value.is_a?(Hash) || value.is_a?(Array)
209
+ if header_name == "Content-Type"
210
+ values = values_to_s(value, ", ")
211
+ else
212
+ values = values_to_s(value)
213
+ end
214
+
215
+ result << "#{header_name}: #{values}\r\n"
216
+ else
217
+ result << "#{header_name}: #{value}\r\n"
218
+ end
219
+
220
+ result
221
+ end
222
+
223
+ arr = header_string.split "\r\n"
224
+ # Move the Session header to the top
225
+ session_index = arr.index { |a| a =~ /Session/ }
226
+ unless session_index.nil?
227
+ session = arr.delete_at(session_index)
228
+ arr.unshift(session)
229
+ end
230
+
231
+ # Move the User-Agent header to the top
232
+ user_agent_index = arr.index { |a| a =~ /User-Agent/ }
233
+ unless user_agent_index.nil?
234
+ user_agent = arr.delete_at(user_agent_index)
235
+ arr.unshift(user_agent)
236
+ end
237
+
238
+ # Move the CSeq header to the top
239
+ cseq_index = arr.index { |a| a =~ /CSeq/ }
240
+ cseq = arr.delete_at(cseq_index)
241
+ arr.unshift(cseq)
242
+
243
+ # Put it all back to a String
244
+ header_string = arr.join("\r\n")
245
+ header_string << "\r\n"
246
+ end
247
+
248
+ # Turns header values into a single string.
249
+ #
250
+ # @param [] values The header values to put to string.
251
+ # @param [String] separator The character to use to separate multiple values
252
+ # that define a header.
253
+ # @return [String] The header values as a single string.
254
+ def values_to_s(values, separator=";")
255
+ result = values.inject("") do |values_string, (header_field, header_field_value)|
256
+ if header_field.is_a? Symbol
257
+ values_string << "#{header_field}=#{header_field_value}"
258
+ elsif header_field.is_a? Hash
259
+ values_string << values_to_s(header_field)
260
+ else
261
+ values_string << header_field.to_s
262
+ end
263
+
264
+ values_string + separator
265
+ end
266
+
267
+ result.sub!(/#{separator}$/, '') if result.end_with? separator
268
+ end
269
+ end
270
+ end
data/lib/rtsp/response.rb CHANGED
@@ -1,74 +1,134 @@
1
1
  require 'rubygems'
2
- require 'socket'
3
2
  require 'sdp'
3
+ require_relative 'error'
4
4
 
5
5
  module RTSP
6
+
7
+ # Parses raw response data from the server/client and turns it into
8
+ # attr_readers.
6
9
  class Response
10
+ attr_reader :rtsp_version
7
11
  attr_reader :code
8
12
  attr_reader :message
9
13
  attr_reader :body
10
-
11
- def initialize(response)
12
- response_array = response.split "\r\n\r\n"
13
14
 
14
- if response_array.empty?
15
- response_array = response.split "\n\n"
15
+ # @param [String] raw_response The raw response string returned from the
16
+ # server/client.
17
+ def initialize(raw_response)
18
+ if raw_response.nil? || raw_response.empty?
19
+ raise RTSP::Error, "#{self.class} received nil string--this shouldn't happen."
16
20
  end
17
21
 
18
- head = response_array.first
19
- body = response_array.last == head ? "" : response_array.last
22
+ @raw_response = raw_response
23
+
24
+ head, body = split_head_and_body_from @raw_response
20
25
  parse_head(head)
21
26
  @body = parse_body(body)
22
27
  end
23
28
 
24
- # Reads through each line of the RTSP response and creates a
25
- # snake-case accessor with that value set.
29
+ # @return [String] The unparsed response as a String.
30
+ def to_s
31
+ @raw_response
32
+ end
33
+
34
+ # Custom redefine to make sure all the dynamically created instance
35
+ # variables are displayed when this method is called.
36
+ #
37
+ # @return [String]
38
+ def inspect
39
+ me = "#<#{self.class.name}:#{self.__id__} "
40
+
41
+ self.instance_variables.each do |variable|
42
+ me << "#{variable}=#{instance_variable_get(variable).inspect}, "
43
+ end
44
+
45
+ me.sub!(/, $/, "")
46
+ me << ">"
47
+
48
+ me
49
+ end
50
+
51
+ # Takes the raw response text and splits it into a 2-element Array, where 0
52
+ # is the text containing the headers and 1 is the text containing the body.
26
53
  #
27
- # @param [String] head
54
+ # @param [String] raw_response
55
+ # @return [Array<String>] 2-element Array containing the head and body of
56
+ # the response. Body will be nil if there wasn't one in the response.
57
+ def split_head_and_body_from raw_response
58
+ head_and_body = raw_response.split("\r\n\r\n", 2)
59
+ head = head_and_body.first
60
+ body = head_and_body.last == head ? nil : head_and_body.last
61
+
62
+ [head, body]
63
+ end
64
+
65
+ # Pulls out the RTSP version, response code, and response message (AKA the
66
+ # status line info) into instance variables.
67
+ #
68
+ # @param [String] line The String containing the status line info.
69
+ def extract_status_line(line)
70
+ line =~ /RTSP\/(\d\.\d) (\d\d\d) ([^\r\n]+)/
71
+ @rtsp_version = $1
72
+ @code = $2.to_i
73
+ @message = $3
74
+
75
+ if @rtsp_version.nil?
76
+ raise RTSP::Error, "Status line corrupted: #{line}"
77
+ end
78
+ end
79
+
80
+ # Reads through each header line of the RTSP response, extracts the response
81
+ # code, response message, response version, and creates a snake-case
82
+ # accessor with that value set.
83
+ #
84
+ # @param [String] head The section of headers from the response text.
28
85
  def parse_head head
29
86
  lines = head.split "\r\n"
30
87
 
31
88
  lines.each_with_index do |line, i|
32
89
  if i == 0
33
- line =~ /RTSP\/1.0 (\d\d\d) ([^\r\n]+)/
34
- @code = $1.to_i
35
- @message = $2
90
+ extract_status_line(line)
36
91
  next
37
92
  end
38
93
 
39
94
  if line.include? ": "
40
- header_field = line.strip.split(": ")
41
- header_name = header_field.first.downcase.gsub(/-/, "_")
42
- create_reader(header_name, header_field.last)
95
+ header_and_value = line.strip.split(":", 2)
96
+ header_name = header_and_value.first.downcase.gsub(/-/, "_")
97
+ create_reader(header_name, header_and_value[1].strip)
43
98
  end
44
99
  end
45
100
  end
46
101
 
102
+ # Reads through each line of the RTSP response body and parses it if
103
+ # needed. Returns a SDP::Description if the Content-Type is
104
+ # 'application/sdp', otherwise returns the String that was passed in.
105
+ #
106
+ # @param [String] body
107
+ # @return [SDP::Description,String]
47
108
  def parse_body body
48
- #response[:body] = read_nonblock(size).split("\r\n") unless @content_length == 0
49
109
  if body =~ /^(\r\n|\n)/
50
110
  body.gsub!(/^(\r\n|\n)/, '')
51
111
  end
52
112
 
53
113
  if @content_type == "application/sdp"
54
114
  SDP.parse body
115
+ else
116
+ body
55
117
  end
56
118
  end
57
119
 
58
- # @param [Number] size
59
- # @param [Hash] options
60
- # @option options [Number] time Duration to read on the non-blocking socket.
61
- def read_nonblock(size, options={})
62
- options[:time] ||= 1
63
- buffer = nil
64
- timeout(options[:time]) { buffer = @socket.read_nonblock(size) }
65
-
66
- buffer
67
- end
68
-
69
120
  private
70
121
 
122
+ # Creates an attr_reader with the name given and sets it to the value that's
123
+ # given.
124
+ #
125
+ # @param [String] name
126
+ # @param [String] value
71
127
  def create_reader(name, value)
128
+ unless value.empty?
129
+ value = value =~ /^[0-9]*$/ ? value.to_i : value
130
+ end
131
+
72
132
  instance_variable_set("@#{name}", value)
73
133
  self.instance_eval "def #{name}; @#{name}; end"
74
134
  end