rtsp_server 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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 +303 -0
  27. data/lib/rtsp/socat_streaming.rb +309 -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 +42 -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