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
@@ -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