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
@@ -0,0 +1,3 @@
1
+ Before do
2
+ @fake_server = FakeRTSPServer.new
3
+ end
data/gemspec.yml ADDED
@@ -0,0 +1,30 @@
1
+ name: rtsp
2
+ summary: "Library to allow RTSP streaming from RTSP-enabled devices."
3
+ description: "This library intends to follow the RTSP RFC document (2326) to allow for working
4
+ with RTSP servers. At this point, it's up to you to parse the data from a play
5
+ call, but we'll get there. ...eventually.
6
+
7
+ For more information
8
+
9
+ RTSP: http://www.ietf.org/rfc/rfc2326.txt"
10
+ license: MIT
11
+ authors: Steve Loveless, Mike Kirby
12
+ email: steve.loveless@gmail.com, mkiby@gmail.com
13
+ homepage: http://rubygems.org/gems/rtsp
14
+ has_yard: true
15
+ executables: 'bin/rtsp_client'
16
+
17
+ dependencies:
18
+ sdp: ~> 0.2.0
19
+
20
+ development_dependencies:
21
+ bundler: ~> 1.0.0
22
+ code_statistics: ~> 0.2.13
23
+ metric_fu: '>= 2.0.0'
24
+ ore: ~> 0.7.2
25
+ ore-core: ~> 0.1.5
26
+ ore-tasks: ~> 0.5.0
27
+ rake: ~> 0.8.7
28
+ rspec: ~> 2.5.0
29
+ simplecov: '>= 0.4.0'
30
+ yard: ~> 0.6.0
data/lib/ext/logger.rb ADDED
@@ -0,0 +1,8 @@
1
+ require 'logger'
2
+
3
+ class Logger
4
+ # Redefining to output a smaller timestamp.
5
+ def format_message(level, time, progname, msg)
6
+ "[#{time}] #{msg.to_s}\n"
7
+ end
8
+ end
data/lib/rtsp.rb CHANGED
@@ -1,12 +1,9 @@
1
1
  require 'pathname'
2
2
 
3
+ require File.expand_path(File.dirname(__FILE__) + '/rtsp/version')
4
+
3
5
  # This base module simply defines properties about the library. See child
4
6
  # classes/modules for the meat.
5
7
  module RTSP
6
- VERSION = '0.0.1.alpha'
7
- WWW = 'http://github.com/turboladen/rtsp'
8
- LIBRARY_ROOT = File.dirname(__FILE__)
9
- PROJECT_ROOT = Pathname.new(LIBRARY_ROOT).parent
10
-
11
- RTSP_VERSION = '1.0'
8
+
12
9
  end
@@ -0,0 +1,105 @@
1
+ require 'tempfile'
2
+ require 'socket'
3
+
4
+ require_relative 'error'
5
+
6
+ module RTSP
7
+
8
+ # Objects of this type can be used with a +RTSP::Client+ object in order to
9
+ # capture the RTP data transmitted to the client as a result of an RTSP
10
+ # PLAY call.
11
+ #
12
+ # In this version, objects of this type don't do much other than just capture
13
+ # the data to a file; in later versions, objects of this type will be able
14
+ # to provide a "sink" and allow for ensuring that the received RTP packets
15
+ # will be reassembled in the correct order, as they're written to file
16
+ # (objects of this type don't don't currently check RTP sequence numbers
17
+ # on the data that's been received).
18
+ class Capturer
19
+
20
+ # Name of the file the data will be captured to unless #rtp_file is set.
21
+ DEFAULT_CAPFILE_NAME = "rtsp_capture.rtsp"
22
+
23
+ # Maximum number of bytes to receive on the socket.
24
+ MAX_BYTES_TO_RECEIVE = 3000
25
+
26
+ # @param [File] rtp_file The file to capture the RTP data to.
27
+ # @return [File]
28
+ attr_accessor :rtp_file
29
+
30
+ # @param [Fixnum] rtp_port The port on which to capture the RTP data.
31
+ # @return [Fixnum]
32
+ attr_accessor :rtp_port
33
+
34
+ # @param [Symbol] transport_protocol +:UDP+ or +:TCP+.
35
+ # @return [Symbol]
36
+ attr_accessor :transport_protocol
37
+
38
+ # @param [Symbol] broadcast_type +:multicast+ or +:unicast+.
39
+ # @return [Symbol]
40
+ attr_accessor :broadcast_type
41
+
42
+ # @param [Symbol] transport_protocol The type of socket to use for capturing
43
+ # the data. +:UDP+ or +:TCP+.
44
+ # @param [Fixnum] rtp_port The port on which to capture RTP data.
45
+ # @param [File] capture_file The file object to capture the RTP data to.
46
+ def initialize(transport_protocol=:UDP, rtp_port=9000, rtp_capture_file=nil)
47
+ @transport_protocol = transport_protocol
48
+ @rtp_port = rtp_port
49
+ @rtp_file = rtp_capture_file || Tempfile.new(DEFAULT_CAPFILE_NAME)
50
+ end
51
+
52
+ # Initializes a server of the correct socket type.
53
+ #
54
+ # @return [UDPSocket, TCPSocket]
55
+ # @raise [RTSP::Error] If +@transport_protocol was not set to +:UDP+ or
56
+ # +:TCP+.
57
+ def init_server
58
+ if @transport_protocol == :UDP
59
+ server = init_udp_server
60
+ elsif @transport_protocol == :tcp
61
+ server = init_tcp_server
62
+ else
63
+ raise RTSP::Error, "Unknown streaming_protocol requested: #{@transport_protocol}"
64
+ end
65
+
66
+ server
67
+ end
68
+
69
+ # Starts capturing data on +@rtp_port+ and writes it to +@rtp_file+.
70
+ def run
71
+ server = init_server
72
+
73
+ loop do
74
+ data = server.recvfrom(MAX_BYTES_TO_RECEIVE).first
75
+ RTSP::Client.log data.size
76
+ @rtp_file.write data
77
+ end
78
+ end
79
+
80
+ # Sets up to receive data on a UDP socket, using +@rtp_port+.
81
+ #
82
+ # @return [UDPSocket]
83
+ def init_udp_server
84
+ server = UDPSocket.open
85
+ server.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
86
+ server.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEPORT, true)
87
+ server.bind('0.0.0.0', @rtp_port)
88
+ RTSP::Client.log "UDP server setup to receive on port #{@rtp_port}"
89
+
90
+ server
91
+ end
92
+
93
+ # Sets up to receive data on a TCP socket, using +@rtp_port+.
94
+ #
95
+ # @return [TCPSocket]
96
+ def init_tcp_server
97
+ server = TCPSocket.new('0.0.0.0', @rtp_port)
98
+ server.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
99
+ server.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEPORT, true)
100
+ RTSP::Client.log "TCP server setup to receive on port #{@rtp_port}"
101
+
102
+ server
103
+ end
104
+ end
105
+ end
data/lib/rtsp/client.rb CHANGED
@@ -1,272 +1,514 @@
1
- require 'rubygems'
2
- require 'logger'
3
1
  require 'socket'
4
2
  require 'tempfile'
5
3
  require 'timeout'
6
- require 'uri'
7
4
 
8
- require 'rtsp/request_messages'
9
- require 'rtsp/response'
5
+ require_relative 'transport_parser'
6
+ require_relative 'capturer'
7
+ require_relative 'error'
8
+ require_relative 'global'
9
+ require_relative 'helpers'
10
+ require_relative 'message'
11
+ require_relative 'response'
10
12
 
11
13
  module RTSP
12
14
 
13
- # Allows for pulling streams from an RTSP server.
15
+ # This is the main interface to an RTSP server. A client object uses a couple
16
+ # main objects for configuration: an +RTSP::Capturer+ and a Connection Struct.
17
+ # Use the capturer to configure how to capture the data which is the RTP
18
+ # stream provided by the RTSP server. Use the connection object to control
19
+ # the connection to the server.
20
+ #
21
+ # You can initialize your client object using a block:
22
+ # client = RTSP::Client.new("rtsp://192.168.1.10") do |connection, capturer|
23
+ # connection.timeout = 5
24
+ # capturer.rtp_file = File.open("my_file.rtp", "wb")
25
+ # end
26
+ #
27
+ # ...or, without the block:
28
+ # client = RTSP::Client.new("rtsp://192.168.1.10")
29
+ # client.connection.timeout = 5
30
+ # client.capturer.rtp_file = File.open("my_file.rtp", "wb")
31
+ #
32
+ # After setting up the client object, call RTSP methods, Ruby style:
33
+ # client.options
34
+ #
35
+ # Remember that, unlike HTTP, RTSP is state-based (and thus the ability to
36
+ # call certain methods depends on calling other methods first). Your client
37
+ # object tells you the current RTSP state that it's in:
38
+ # client.options
39
+ # client.session_state # => :init
40
+ # client.describe
41
+ # client.session_state # => :init
42
+ # client.setup(client.media_control_tracks.first)
43
+ # client.session_state # => :ready
44
+ # client.play(client.aggregate_control_track)
45
+ # client.session_state # => :playing
46
+ # client.pause(client.aggregate_control_track)
47
+ # client.session_state # => :ready
48
+ # client.teardown(client.aggregate_control_track)
49
+ # client.session_state # => :init
50
+ #
51
+ # To enable/disable logging for clients, class methods:
52
+ # RTSP::Client.log? # => true
53
+ # RTSP::Client.log = false
54
+ # @todo Break Stream out in to its own class.
14
55
  class Client
15
- include RTSP::RequestMessages
16
-
17
- MAX_BYTES_TO_RECEIVE = 1500
18
-
19
- attr_reader :server_uri
20
- attr_accessor :stream_tracks
21
-
22
- # @param [String] rtsp_url URL to the resource to stream. If no scheme is given,
23
- # "rtsp" is assumed. If no port is given, 554 is assumed. If no path is
24
- # given, "/stream1"is assumed.
25
- def initialize(rtsp_url, options={})
26
- @server_uri = build_server_uri(rtsp_url)
27
- @socket = options[:socket] || TCPSocket.new(@server_uri.host,
28
- @server_uri.port)
29
- @stream_tracks = options[:stream_tracks] || ["/track1"]
30
- @timeout = options[:timeout] || 2
31
- @session
32
- @logger = Logger.new(STDOUT)
33
-
34
- if options[:capture_file_path] && options[:capture_duration]
35
- @capture_file_path = options[:capture_file_path]
36
- @capture_duration = options[:capture_duration]
37
- setup_capture
38
- end
39
- end
56
+ include RTSP::Helpers
57
+ extend RTSP::Global
40
58
 
41
- def setup_capture
42
- @capture_file = File.open(@capture_file_path, File::WRONLY|File::EXCL|File::CREAT)
43
- @capture_socket = UDPSocket.new
44
- @capture_socket.bind "0.0.0.0", @server_uri.port
45
- end
59
+ DEFAULT_CAPFILE_NAME = "ruby_rtsp_capture.rtsp"
60
+ MAX_BYTES_TO_RECEIVE = 3000
46
61
 
47
- # TODO: update sequence
48
- # @return [Hash] The response formatted as a Hash.
49
- def options
50
- @logger.debug "Sending OPTIONS to #{@server_uri.host}#{@stream_path}"
51
- response = send_rtsp RequestMessages.options(@server_uri.to_s)
52
- @logger.debug "Recieved response:"
53
- @logger.debug response
62
+ # @return [URI] The URI that points to the RTSP server's resource.
63
+ attr_reader :server_uri
54
64
 
55
- @session = response.cseq
65
+ # @return [Fixnum] Also known as the "sequence" number, this starts at 1 and
66
+ # increments after every request to the server. It is reset after
67
+ # calling #teardown.
68
+ attr_reader :cseq
56
69
 
57
- response
58
- end
70
+ # @return [Fixnum] A session is only established after calling #setup;
71
+ # otherwise returns nil.
72
+ attr_reader :session
59
73
 
60
- # TODO: update sequence
61
- # TODO: get tracks, IP's, ports, multicast/unicast
62
- # @return [Hash] The response formatted as a Hash.
63
- def describe
64
- @logger.debug "Sending DESCRIBE to #{@server_uri.host}#{@stream_path}"
65
- response = send_rtsp(RequestMessages.describe("#{@server_uri.to_s}#{@stream_path}"))
74
+ # @return [Array<Symbol>] Only populated after calling #options; otherwise
75
+ # returns nil. There's no sense in making any other requests than these
76
+ # since the server doesn't support them.
77
+ attr_reader :supported_methods
66
78
 
67
- @logger.debug "Recieved response:"
68
- @logger.debug response.inspect
79
+ # @return [Struct::Connection]
80
+ attr_accessor :connection
69
81
 
70
- @session = response.cseq
71
- @sdp_info = response.body
72
- @content_base = response.content_base
82
+ # Use to get/set an object for capturing received data.
83
+ # @param [RTSP::Capturer]
84
+ # @return [RTSP::Capturer]
85
+ attr_accessor :capturer
73
86
 
74
- response
75
- end
87
+ # @return [Symbol] See {RFC section A.1.}[http://tools.ietf.org/html/rfc2326#page-76]
88
+ attr_reader :session_state
76
89
 
77
- # TODO: update sequence
78
- # TODO: get session
79
- # TODO: parse Transport header (http://tools.ietf.org/html/rfc2326#section-12.39)
80
- # @return [Hash] The response formatted as a Hash.
81
- def setup(options={})
82
- @logger.debug "Sending SETUP to #{@server_uri.host}#{@stream_path}"
83
- setup_url = @content_base || "#{@server_uri.to_s}#{@stream_path}"
84
- response = send_rtsp RequestMessages.setup(setup_url, options)
85
-
86
- @logger.debug "Recieved response:"
87
- @logger.debug response
90
+ # Use to configure options for all clients.
91
+ # @see RTSP::Global
92
+ def self.configure
93
+ yield self if block_given?
94
+ end
88
95
 
89
- @session = response.cseq
96
+ # @param [String] server_url URL to the resource to stream. If no scheme is
97
+ # given, "rtsp" is assumed. If no port is given, 554 is assumed.
98
+ # @yield [Struct::Connection, RTSP::Capturer]
99
+ # @yieldparam [Struct::Connection] server_url=
100
+ # @yieldparam [Struct::Connection] timeout=
101
+ # @yieldparam [Struct::Connection] socket=
102
+ # @yieldparam [Struct::Connection] do_capture=
103
+ # @yieldparam [Struct::Connection] interleave=
104
+ # @todo Use server_url everywhere; just use URI to ensure the port & rtspu.
105
+ def initialize(server_url=nil)
106
+ Thread.abort_on_exception = true
107
+
108
+ Struct.new("Connection", :server_url, :timeout, :socket,
109
+ :do_capture, :interleave)
110
+ @connection = Struct::Connection.new
111
+ @capturer = RTSP::Capturer.new
112
+
113
+ yield(connection, capturer) if block_given?
114
+
115
+ @connection.server_url = server_url || @connection.server_url
116
+ @server_uri = build_resource_uri_from(@connection.server_url)
117
+ @connection.timeout ||= 30
118
+ @connection.socket ||= TCPSocket.new(@server_uri.host, @server_uri.port)
119
+ @connection.do_capture ||= true
120
+ @connection.interleave ||= false
121
+ @capturer.rtp_port ||= 9000
122
+ @capturer.transport_protocol ||= :UDP
123
+ @capturer.broadcast_type ||= :unicast
124
+ @capturer.rtp_file ||= Tempfile.new(DEFAULT_CAPFILE_NAME)
125
+
126
+ @play_thread = nil
127
+ @cseq = 1
128
+ reset_state
129
+ end
90
130
 
91
- response
131
+ # The URL for the RTSP server to talk to can change if multiple servers are
132
+ # involved in delivering content. This method can be used to change the
133
+ # server to talk to on the fly.
134
+ #
135
+ # @param [String] new_url The new server URL to use to communicate over.
136
+ def server_url=(new_url)
137
+ @server_uri = build_resource_uri_from new_url
92
138
  end
93
139
 
94
- # TODO: update sequence
95
- # TODO: get session
96
- # @return [Hash] The response formatted as a Hash.
97
- def play(options={})
98
- @logger.debug "Sending PLAY to #{@server_uri.host}#{@stream_path}"
99
- session = options[:session] || @session
100
- response = send_rtsp RequestMessages.play(@server_uri.to_s,
101
- options[:session])
102
-
103
- @logger.debug "Recieved response:"
104
- @logger.debug response
105
- @session = response.cseq
106
-
107
- if @capture_file_path
108
- begin
109
- Timeout::timeout(@capture_duration) do
110
- while data = @capture_socket.recvfrom(102400).first
111
- @logger.debug "data size = #{data.size}"
112
- @capture_file_path.write data
113
- end
114
- end
115
- rescue Timeout::Error
116
- # Blind rescue
140
+ # Sends the message over the socket.
141
+ #
142
+ # @param [RTSP::Message] message
143
+ # @return [RTSP::Response]
144
+ # @raise [RTSP::Error] If the timeout value is reached and the server hasn't
145
+ # responded.
146
+ def send_message message
147
+ RTSP::Client.log "Sending #{message.method_type.upcase} to #{message.request_uri}"
148
+ message.to_s.each_line { |line| RTSP::Client.log line.strip }
149
+
150
+ begin
151
+ response = Timeout::timeout(@connection.timeout) do
152
+ @connection.socket.send(message.to_s, 0)
153
+ socket_data = @connection.socket.recvfrom MAX_BYTES_TO_RECEIVE
154
+ RTSP::Response.new socket_data.first
117
155
  end
118
-
119
- @capture_socket.close
156
+ rescue Timeout::Error
157
+ raise RTSP::Error, "Request took more than #{@connection.timeout} seconds to send."
120
158
  end
121
159
 
122
- response
123
- end
160
+ RTSP::Client.log "Received response:"
124
161
 
125
- def pause(options={})
126
- @logger.debug "Sending PAUSE to #{@server_uri.host}#{@stream_path}"
127
- response = send_rtsp RequestMessages.pause(@stream_tracks.first,
128
- options[:session],
129
- options[:sequence])
130
-
131
- @logger.debug "Recieved response:"
132
- @logger.debug response
133
- @session = response.cseq
162
+ if response
163
+ response.to_s.each_line { |line| RTSP::Client.log line.strip }
164
+ end
134
165
 
135
166
  response
136
167
  end
137
168
 
138
- # @return [Hash] The response formatted as a Hash.
139
- def teardown
140
- @logger.debug "Sending TEARDOWN to #{@server_uri.host}#{@stream_path}"
141
- response = send_rtsp RequestMessages.teardown(@server_uri.to_s, @session)
142
- @logger.debug "Recieved response:"
143
- @logger.debug response
144
- #@socket.close if @socket.open?
145
- @socket = nil
146
-
147
- response
169
+ # Sends an OPTIONS message to the server specified by +@server_uri+. Sets
170
+ # +@supported_methods+ based on the list of supported methods returned in
171
+ # the Public headers.
172
+ #
173
+ # @param [Hash] additional_headers
174
+ # @return [RTSP::Response]
175
+ # @see http://tools.ietf.org/html/rfc2326#page-30 RFC 2326, Section 10.1.
176
+ def options(additional_headers={})
177
+ message = RTSP::Message.options(@server_uri.to_s).with_headers({
178
+ cseq: @cseq })
179
+ message.add_headers additional_headers
180
+
181
+ request(message) do |response|
182
+ @supported_methods = extract_supported_methods_from response.public
183
+ end
148
184
  end
149
185
 
150
- =begin
151
- def connect
152
- timeout(@timeout) { @socket = TCPSocket.new(@host, @port) } #rescue @socket = nil
186
+ # Sends the DESCRIBE request, then extracts the SDP description into
187
+ # +@session_description+, extracts the session +@start_time+ and +@stop_time+,
188
+ # +@content_base+, +@media_control_tracks+, and +@aggregate_control_track+.
189
+ #
190
+ # @todo get tracks, IP's, ports, multicast/unicast
191
+ # @param [Hash] additional_headers
192
+ # @return [RTSP::Response]
193
+ # @see http://tools.ietf.org/html/rfc2326#page-31 RFC 2326, Section 10.2.
194
+ # @see #media_control_tracks
195
+ # @see #aggregate_control_track
196
+ def describe additional_headers={}
197
+ message = RTSP::Message.describe(@server_uri.to_s).with_headers({
198
+ cseq: @cseq })
199
+ message.add_headers additional_headers
200
+
201
+ request(message) do |response|
202
+ @session_description = response.body
203
+ #@session_start_time = response.body.start_time
204
+ #@session_stop_time = response.body.stop_time
205
+ @content_base = build_resource_uri_from response.content_base
206
+
207
+ @media_control_tracks = media_control_tracks
208
+ @aggregate_control_track = aggregate_control_track
209
+ end
153
210
  end
154
211
 
155
- def connected?
156
- @socket == nil ? true : false
212
+ # Sends an ANNOUNCE Request to the provided URL. This method also requires
213
+ # an SDP description to send to the server.
214
+ #
215
+ # @param [String] request_url The URL to post the presentation or media
216
+ # object to.
217
+ # @param [SDP::Description] description The SDP description to send to the
218
+ # server.
219
+ # @param [Hash] additional_headers
220
+ # @return [RTSP::Response]
221
+ # @see http://tools.ietf.org/html/rfc2326#page-32 RFC 2326, Section 10.3.
222
+ def announce(request_url, description, additional_headers={})
223
+ message = RTSP::Message.announce(request_url).with_headers({ cseq: @cseq })
224
+ message.add_headers additional_headers
225
+ message.body = description.to_s
226
+
227
+ request(message)
157
228
  end
158
229
 
159
- def disconnect
160
- timeout(@timeout) { @socket.close } rescue @socket = nil
230
+ # Builds the Transport header fields string based on info used in setting up
231
+ # the Client instance.
232
+ #
233
+ # @return [String] The String to use with the Transport header.
234
+ # @see http://tools.ietf.org/html/rfc2326#page-58 RFC 2326, Section 12.39.
235
+ def request_transport
236
+ value = "RTP/AVP;#{@capturer.broadcast_type};client_port="
237
+ value << "#{@capturer.rtp_port}-#{@capturer.rtp_port + 1}\r\n"
161
238
  end
162
- =end
163
239
 
164
- # @param []
165
- def send_rtsp(message)
166
- message.each_line { |line| @logger.debug line }
167
- recv if timeout(@timeout) { @socket.send(message, 0) }
240
+ # Sends the SETUP request, then sets +@session+ to the value returned in the
241
+ # Session header from the server, then sets the +@session_state+ to +:ready+.
242
+ #
243
+ # @todo +@session+ numbers are relevant to tracks, and a client must be able
244
+ # to play multiple tracks at the same time.
245
+ # @param [String] track
246
+ # @param [Hash] additional_headers
247
+ # @return [RTSP::Response] The response formatted as a Hash.
248
+ # @see http://tools.ietf.org/html/rfc2326#page-33 RFC 2326, Section 10.4.
249
+ def setup(track, additional_headers={})
250
+ message = RTSP::Message.setup(track).with_headers({
251
+ cseq: @cseq, transport: request_transport })
252
+ message.add_headers additional_headers
253
+
254
+ request(message) do |response|
255
+ if @session_state == :init
256
+ @session_state = :ready
257
+ end
258
+
259
+ @session = response.session
260
+ parser = RTSP::TransportParser.new
261
+ @transport = parser.parse response.transport
262
+
263
+ unless @transport[:transport_protocol].nil?
264
+ @capturer.transport_protocol = @transport[:transport_protocol]
265
+ end
266
+
267
+ @capturer.rtp_port = @transport[:client_port][:rtp].to_i
268
+ @capturer.broadcast_type = @transport[:broadcast_type]
269
+ end
168
270
  end
169
271
 
170
- def aggregate_control_track
171
- aggregate_control = @sdp_info.attributes.find_all do |a|
172
- a[:attribute] == "control"
272
+ # Sends the PLAY request and sets +@session_state+ to +:playing+.
273
+ #
274
+ # @param [String] track
275
+ # @param [Hash] additional_headers
276
+ # @return [RTSP::Response]
277
+ # @todo If playback over UDP doesn't result in any data coming in on the
278
+ # socket, re-setup with RTP/AVP/TCP;unicast;interleaved=0-1.
279
+ # @raise [RTSP::Error] If +#play+ is called but the session hasn't yet been
280
+ # set up via +#setup+.
281
+ # @see http://tools.ietf.org/html/rfc2326#page-34 RFC 2326, Section 10.5.
282
+ def play(track, additional_headers={})
283
+ message = RTSP::Message.play(track).with_headers({
284
+ cseq: @cseq, session: @session })
285
+ message.add_headers additional_headers
286
+
287
+ request(message) do
288
+ unless @session_state == :ready
289
+ raise RTSP::Error, "Session not set up yet. Run #setup first."
290
+ end
291
+
292
+ if @play_thread.nil?
293
+ RTSP::Client.log "Capturing RTP data on port #{@transport[:client_port][:rtp]}"
294
+
295
+ @play_thread = Thread.new do
296
+ @capturer.run
297
+ end
298
+ end
299
+
300
+ @session_state = :playing
173
301
  end
302
+ end
174
303
 
175
- aggregate_control.first[:value]
304
+ # Sends the PAUSE request and sets +@session_state+ to +:ready+.
305
+ #
306
+ # @param [String] track A track or presentation URL to pause.
307
+ # @param [Hash] additional_headers
308
+ # @return [RTSP::Response]
309
+ # @see http://tools.ietf.org/html/rfc2326#page-36 RFC 2326, Section 10.6.
310
+ def pause(track, additional_headers={})
311
+ message = RTSP::Message.pause(track).with_headers({
312
+ cseq: @cseq, session: @session })
313
+ message.add_headers additional_headers
314
+
315
+ request(message) do
316
+ if [:playing, :recording].include? @session_state
317
+ @session_state = :ready
318
+ end
319
+ end
176
320
  end
177
321
 
178
- def media_control_tracks
179
- tracks = []
180
- @sdp_info.media_sections.each do |media_section|
181
- media_section[:attributes].each do |a|
182
- tracks << a[:value] if a[:attribute] == "control"
322
+ # Sends the TEARDOWN request, then resets all state-related instance
323
+ # variables.
324
+ #
325
+ # @param [String] track The presentation or media track to teardown.
326
+ # @param [Hash] additional_headers
327
+ # @return [RTSP::Response]
328
+ # @see http://tools.ietf.org/html/rfc2326#page-37 RFC 2326, Section 10.7.
329
+ def teardown(track, additional_headers={})
330
+ message = RTSP::Message.teardown(track).with_headers({
331
+ cseq: @cseq, session: @session })
332
+ message.add_headers additional_headers
333
+
334
+ request(message) do
335
+ reset_state
336
+ if @play_thread
337
+ @capturer.rtp_file.close
338
+ @play_thread.exit
183
339
  end
184
340
  end
341
+ end
185
342
 
186
- tracks
343
+ # Sets state related variables back to their starting values;
344
+ # +@session_state+ is set to +:init+; +@session+ is set to 0.
345
+ def reset_state
346
+ @session_state = :init
347
+ @session = 0
187
348
  end
188
349
 
189
- # @return [Hash]
190
- def recv
191
- size = 0
192
- socket_data, sender_sockaddr = @socket.recvfrom MAX_BYTES_TO_RECEIVE
193
- response = RTSP::Response.new socket_data
194
-
195
- #unless response.message == "OK"
196
- # message = "Did not recieve RTSP/1.0 200 OK. Instead got '#{response.status}'"
197
- # message = message + "Full response:\n#{response.inspect}"
198
- # raise message
199
- #
200
- # end
201
- =begin
202
- response = parse_header
203
- unless response[:status].include? "RTSP/1.0 200 OK"
204
- message = "Did not recieve RTSP/1.0 200 OK. Instead got '#{response[:status]}'"
205
- message = message + "Full response:\n#{response}"
206
- raise message
207
- end
350
+ # Sends the GET_PARAMETERS request.
351
+ #
352
+ # @param [String] track The presentation or media track to ping.
353
+ # @param [String] body The string containing the parameters to send.
354
+ # @param [Hash] additional_headers
355
+ # @return [RTSP::Response]
356
+ # @see http://tools.ietf.org/html/rfc2326#page-37 RFC 2326, Section 10.8.
357
+ def get_parameter(track, body="", additional_headers={})
358
+ message = RTSP::Message.get_parameter(track).with_headers({
359
+ cseq: @cseq })
360
+ message.add_headers additional_headers
361
+ message.body = body
362
+
363
+ request(message)
364
+ end
365
+
366
+ # Sends the SET_PARAMETERS request.
367
+ #
368
+ # @param [String] track The presentation or media track to teardown.
369
+ # @param [String] parameters The string containing the parameters to send.
370
+ # @param [Hash] additional_headers
371
+ # @return [RTSP::Response]
372
+ # @see http://tools.ietf.org/html/rfc2326#page-38 RFC 2326, Section 10.9.
373
+ def set_parameter(track, parameters, additional_headers={})
374
+ message = RTSP::Message.set_parameter(track).with_headers({
375
+ cseq: @cseq })
376
+ message.add_headers additional_headers
377
+ message.body = parameters
378
+
379
+ request(message)
380
+ end
208
381
 
209
- response[:status] = readline
210
- while line = readline
211
- break if line == "\r\n"
382
+ # Sends the RECORD request and sets +@session_state+ to +:recording+.
383
+ #
384
+ # @param [String] track
385
+ # @param [Hash] additional_headers
386
+ # @return [RTSP::Response]
387
+ # @see http://tools.ietf.org/html/rfc2326#page-39 RFC 2326, Section 10.11.
388
+ def record(track, additional_headers={})
389
+ message = RTSP::Message.record(track).with_headers({
390
+ cseq: @cseq, session: @session })
391
+ message.add_headers additional_headers
392
+
393
+ request(message) { @session_state = :recording }
394
+ end
212
395
 
213
- if line.include? ": "
214
- a = line.strip().split(": ")
215
- response.merge!({a[0].downcase => a[1]})
396
+ # Executes the Request with the arguments passed in, yields the response to
397
+ # the calling block, checks the CSeq response and the session response,
398
+ # then increments +@cseq+ by 1. Handles any exceptions raised during the
399
+ # Request.
400
+ #
401
+ # @param [Hash] new_args
402
+ # @yield [RTSP::Response]
403
+ # @return [RTSP::Response]
404
+ # @raise [RTSP::Error] All 4xx & 5xx response codes & their messages.
405
+ def request message
406
+ response = send_message message
407
+ #compare_sequence_number response.cseq
408
+ @cseq += 1
409
+
410
+ if response.code.to_s =~ /2../
411
+ yield response if block_given?
412
+ elsif response.code.to_s =~ /(4|5)../
413
+ if (defined? response.connection) && response.connection == 'Close'
414
+ reset_state
216
415
  end
416
+
417
+ raise RTSP::Error, "#{response.code}: #{response.message}"
418
+ else
419
+ raise RTSP::Error, "Unknown Response code: #{response.code}"
217
420
  end
218
421
 
219
- size = response["content-length"].to_i if response.has_key?("content-length")
220
- response[:body] = read_nonblock(size).split("\r\n") unless size == 0
422
+ dont_ensure_list = [:options, :describe, :teardown, :set_parameter,
423
+ :get_parameter]
424
+ unless dont_ensure_list.include? message.method_type
425
+ ensure_session
426
+ end
221
427
 
222
428
  response
223
- =end
224
- size = response.content_length.to_i if response.respond_to? 'content_length'
225
- #response[:body] = read_nonblock(size).split("\r\n") unless size == 0
429
+ end
226
430
 
227
- response
431
+ # Ensures that +@session+ is set before continuing on.
432
+ #
433
+ # @raise [RTSP::Error] Raises if @session isn't set.
434
+ def ensure_session
435
+ unless @session > 0
436
+ raise RTSP::Error, "Session number not retrieved from server yet. Run SETUP first."
437
+ end
228
438
  end
229
439
 
230
- # @param [Number] size
231
- # @param [Hash] options
232
- # @option options [Number] time Duration to read on the non-blocking socket.
233
- =begin
234
- def read_nonblock(size, options={})
235
- options[:time] ||= 1
236
- buffer = nil
237
- timeout(options[:time]) { buffer = @socket.read_nonblock(size) }
440
+ # Extracts the URL associated with the "control" attribute from the main
441
+ # section of the session description.
442
+ #
443
+ # @return [String] The URL as a String.
444
+ def aggregate_control_track
445
+ aggregate_control = @session_description.attributes.find_all do |a|
446
+ a[:attribute] == "control"
447
+ end
238
448
 
239
- buffer
449
+ "#{@content_base}#{aggregate_control.first[:value].gsub(/\*/, "")}"
240
450
  end
241
451
 
242
- # @return [String]
243
- def readline(options={})
244
- options[:time] ||= 1
245
- line = nil
246
- timeout(options[:time]) { line = @socket.readline }
452
+ # Extracts the value of the "control" attribute from all media sections of
453
+ # the session description (SDP). You have to call the +#describe+ method in
454
+ # order to get the session description info.
455
+ #
456
+ # @return [Array<String>] The tracks made up of the content base + control
457
+ # track value.
458
+ # @see #describe
459
+ def media_control_tracks
460
+ tracks = []
247
461
 
248
- line
249
- end
250
- =end
251
- #--------------------------------------------------------------------------
252
- # Privates!
253
- private
254
-
255
- def build_server_uri(url)
256
- unless url =~ /^rtsp/
257
- url = "rtsp://#{url}"
462
+ if @session_description.nil?
463
+ tracks << ""
464
+ else
465
+ @session_description.media_sections.each do |media_section|
466
+ media_section[:attributes].each do |a|
467
+ tracks << "#{@content_base}#{a[:value]}" if a[:attribute] == "control"
468
+ end
469
+ end
258
470
  end
259
471
 
260
- server_uri = URI.parse url
261
- server_uri.port ||= 554
472
+ tracks
473
+ end
474
+
475
+ # Compares the sequence number passed in to the current client sequence
476
+ # number ( +@cseq+ ) and raises if they're not equal. If that's the case, the
477
+ # server responded to a different request.
478
+ #
479
+ # @param [Fixnum] server_cseq Sequence number returned by the server.
480
+ # @raise [RTSP::Error] If the server returns a CSeq value that's different
481
+ # from what the client sent.
482
+ def compare_sequence_number server_cseq
483
+ if @cseq != server_cseq
484
+ message = "Sequence number mismatch. Client: #{@cseq}, Server: #{server_cseq}"
485
+ raise RTSP::Error, message
486
+ end
487
+ end
262
488
 
263
- #if @server_uri.path == @server_uri.host
264
- # @server_uri.path = "/stream1"
265
- #else
266
- # @server_uri.path
267
- #end
489
+ # Compares the session number passed in to the current client session
490
+ # number ( +@session+ ) and raises if they're not equal. If that's the case,
491
+ # the server responded to a different request.
492
+ #
493
+ # @param [Fixnum] server_session Session number returned by the server.
494
+ # @raise [RTSP::Error] If the server returns a Session value that's different
495
+ # from what the client sent.
496
+ def compare_session_number server_session
497
+ if @session != server_session
498
+ message = "Session number mismatch. Client: #{@session}, Server: #{server_session}"
499
+ raise RTSP::Error, message
500
+ end
501
+ end
268
502
 
269
- server_uri
503
+ # Takes the methods returned from the Public header from an OPTIONS response
504
+ # and puts them to an Array.
505
+ #
506
+ # @param [String] method_list The string returned from the server containing
507
+ # the list of methods it supports.
508
+ # @return [Array<Symbol>] The list of methods as symbols.
509
+ # @see #options
510
+ def extract_supported_methods_from method_list
511
+ method_list.downcase.split(', ').map { |m| m.to_sym }
270
512
  end
271
513
  end
272
- end
514
+ end