rtsp 0.0.1.alpha → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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