rtsp 0.0.1.alpha
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.
- data/.autotest +23 -0
- data/.infinity_test +4 -0
- data/.rspec +3 -0
- data/Gemfile +12 -0
- data/Gemfile.lock +104 -0
- data/History.txt +4 -0
- data/Manifest.txt +26 -0
- data/PostInstall.txt +7 -0
- data/README.rdoc +74 -0
- data/Rakefile +57 -0
- data/bin/rtsp +121 -0
- data/features/step_definitions/stream_steps.rb +50 -0
- data/features/stream.feature +17 -0
- data/features/support/common.rb +1 -0
- data/features/support/env.rb +37 -0
- data/features/support/world.rb +1 -0
- data/lib/rtsp/client.rb +272 -0
- data/lib/rtsp/request_messages.rb +104 -0
- data/lib/rtsp/response.rb +76 -0
- data/lib/rtsp/status_code.rb +7 -0
- data/lib/rtsp.rb +12 -0
- data/spec/.rspec +1 -0
- data/spec/rtsp/client_spec.rb +38 -0
- data/spec/rtsp/response_spec.rb +189 -0
- data/spec/rtsp_spec.rb +27 -0
- data/spec/spec_helper.rb +11 -0
- metadata +241 -0
data/lib/rtsp/client.rb
ADDED
@@ -0,0 +1,272 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'logger'
|
3
|
+
require 'socket'
|
4
|
+
require 'tempfile'
|
5
|
+
require 'timeout'
|
6
|
+
require 'uri'
|
7
|
+
|
8
|
+
require 'rtsp/request_messages'
|
9
|
+
require 'rtsp/response'
|
10
|
+
|
11
|
+
module RTSP
|
12
|
+
|
13
|
+
# Allows for pulling streams from an RTSP server.
|
14
|
+
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
|
40
|
+
|
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
|
46
|
+
|
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
|
54
|
+
|
55
|
+
@session = response.cseq
|
56
|
+
|
57
|
+
response
|
58
|
+
end
|
59
|
+
|
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}"))
|
66
|
+
|
67
|
+
@logger.debug "Recieved response:"
|
68
|
+
@logger.debug response.inspect
|
69
|
+
|
70
|
+
@session = response.cseq
|
71
|
+
@sdp_info = response.body
|
72
|
+
@content_base = response.content_base
|
73
|
+
|
74
|
+
response
|
75
|
+
end
|
76
|
+
|
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
|
88
|
+
|
89
|
+
@session = response.cseq
|
90
|
+
|
91
|
+
response
|
92
|
+
end
|
93
|
+
|
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
|
117
|
+
end
|
118
|
+
|
119
|
+
@capture_socket.close
|
120
|
+
end
|
121
|
+
|
122
|
+
response
|
123
|
+
end
|
124
|
+
|
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
|
134
|
+
|
135
|
+
response
|
136
|
+
end
|
137
|
+
|
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
|
148
|
+
end
|
149
|
+
|
150
|
+
=begin
|
151
|
+
def connect
|
152
|
+
timeout(@timeout) { @socket = TCPSocket.new(@host, @port) } #rescue @socket = nil
|
153
|
+
end
|
154
|
+
|
155
|
+
def connected?
|
156
|
+
@socket == nil ? true : false
|
157
|
+
end
|
158
|
+
|
159
|
+
def disconnect
|
160
|
+
timeout(@timeout) { @socket.close } rescue @socket = nil
|
161
|
+
end
|
162
|
+
=end
|
163
|
+
|
164
|
+
# @param []
|
165
|
+
def send_rtsp(message)
|
166
|
+
message.each_line { |line| @logger.debug line }
|
167
|
+
recv if timeout(@timeout) { @socket.send(message, 0) }
|
168
|
+
end
|
169
|
+
|
170
|
+
def aggregate_control_track
|
171
|
+
aggregate_control = @sdp_info.attributes.find_all do |a|
|
172
|
+
a[:attribute] == "control"
|
173
|
+
end
|
174
|
+
|
175
|
+
aggregate_control.first[:value]
|
176
|
+
end
|
177
|
+
|
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"
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
tracks
|
187
|
+
end
|
188
|
+
|
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
|
208
|
+
|
209
|
+
response[:status] = readline
|
210
|
+
while line = readline
|
211
|
+
break if line == "\r\n"
|
212
|
+
|
213
|
+
if line.include? ": "
|
214
|
+
a = line.strip().split(": ")
|
215
|
+
response.merge!({a[0].downcase => a[1]})
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
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
|
221
|
+
|
222
|
+
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
|
226
|
+
|
227
|
+
response
|
228
|
+
end
|
229
|
+
|
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) }
|
238
|
+
|
239
|
+
buffer
|
240
|
+
end
|
241
|
+
|
242
|
+
# @return [String]
|
243
|
+
def readline(options={})
|
244
|
+
options[:time] ||= 1
|
245
|
+
line = nil
|
246
|
+
timeout(options[:time]) { line = @socket.readline }
|
247
|
+
|
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}"
|
258
|
+
end
|
259
|
+
|
260
|
+
server_uri = URI.parse url
|
261
|
+
server_uri.port ||= 554
|
262
|
+
|
263
|
+
#if @server_uri.path == @server_uri.host
|
264
|
+
# @server_uri.path = "/stream1"
|
265
|
+
#else
|
266
|
+
# @server_uri.path
|
267
|
+
#end
|
268
|
+
|
269
|
+
server_uri
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
module RTSP
|
2
|
+
|
3
|
+
# This module defines the template strings that make up RTSP methods. Other
|
4
|
+
# objects should use these for building request messages to communicate in
|
5
|
+
# RTSP.
|
6
|
+
module RequestMessages
|
7
|
+
RTSP_VER = "RTSP/1.0"
|
8
|
+
RTSP_ACCEPT_TYPE = "application/sdp"
|
9
|
+
RTP_DEFAULT_PORT = 9000
|
10
|
+
RTP_DEFAULT_PACKET_TYPE = "RTP/AVP"
|
11
|
+
RTP_DEFAULT_ROUTING = "unicast"
|
12
|
+
RTSP_DEFAULT_SEQUENCE_NUMBER = 1
|
13
|
+
RTSP_DEFAULT_NPT = "0.000-"
|
14
|
+
|
15
|
+
def self.options(stream, options={})
|
16
|
+
options[:sequence] ||= RTSP_DEFAULT_SEQUENCE_NUMBER
|
17
|
+
message = "OPTIONS #{stream} #{RTSP_VER}\r\n"
|
18
|
+
message << "CSeq: #{options[:sequence]}\r\n\r\n"
|
19
|
+
end
|
20
|
+
|
21
|
+
# See section 10.2
|
22
|
+
#
|
23
|
+
# @param [String] stream
|
24
|
+
# @param [Hash] options
|
25
|
+
# @option options [Number] sequence
|
26
|
+
# @option options [Array]
|
27
|
+
def self.describe(stream, options={})
|
28
|
+
options[:sequence] ||= RTSP_DEFAULT_SEQUENCE_NUMBER
|
29
|
+
options[:accept] ||= RTSP_ACCEPT_TYPE
|
30
|
+
message = "DESCRIBE #{stream} #{RTSP_VER}\r\n"
|
31
|
+
message << "CSeq: #{options[:sequence]}\r\n"
|
32
|
+
message << "Accept: #{options[:accept]}\r\n\r\n"
|
33
|
+
end
|
34
|
+
|
35
|
+
# @param [String] stream
|
36
|
+
# @param [Number] session
|
37
|
+
# @param [Hash]
|
38
|
+
def self.announce(stream, session, options={})
|
39
|
+
options[:content_type] ||= RTSP_ACCEPT_TYPE
|
40
|
+
message = "ANNOUNCE #{stream} #{RTSP_VER}\r\n"
|
41
|
+
message << "CSeq: #{options[:sequence]}\r\n"
|
42
|
+
message << "Date: "
|
43
|
+
message << "Session: #{session}"
|
44
|
+
message << "Content-Type: #{options[:content_type]}\r\n"
|
45
|
+
message << "Content-Length: #{options[:content_length]}\r\n"
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.setup(track, options={})
|
49
|
+
options[:sequence] ||= RTSP_DEFAULT_SEQUENCE_NUMBER
|
50
|
+
options[:transport] ||= RTP_DEFAULT_PACKET_TYPE
|
51
|
+
options[:port] ||= RTP_DEFAULT_PORT
|
52
|
+
options[:routing] ||= RTP_DEFAULT_ROUTING
|
53
|
+
message = "SETUP #{track} #{RTSP_VER}\r\n"
|
54
|
+
message << "CSeq: #{options[:sequence]}\r\n"
|
55
|
+
message << "Transport: #{options[:transport]};"
|
56
|
+
message << "#{options[:destination]};"
|
57
|
+
message << "client_port=#{options[:port]}-#{options[:port]+1}\r\n\r\n"
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.play(stream, session, options={})
|
61
|
+
options[:sequence] ||= RTSP_DEFAULT_SEQUENCE_NUMBER
|
62
|
+
options[:npt] ||= RTSP_DEFAULT_NPT
|
63
|
+
message = "PLAY #{stream} #{RTSP_VER}\r\n"
|
64
|
+
message << "CSeq: #{options[:sequence]}\r\n"
|
65
|
+
message << "Session: #{session}\r\n"
|
66
|
+
message << "Range: npt=#{options[:npt]}\r\n\r\n"
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.pause(stream, session, sequence)
|
70
|
+
message = "PAUSE #{stream} #{RTSP_VER}\r\n"
|
71
|
+
message << "CSeq: #{sequence}\r\n"
|
72
|
+
message << "Session: #{session}\r\n"
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.teardown(stream, session, options={})
|
76
|
+
options[:sequence] ||= RTSP_DEFAULT_SEQUENCE_NUMBER
|
77
|
+
message = "TEARDOWN #{stream} #{RTSP_VER}\r\n"
|
78
|
+
message << "CSeq: #{options[:sequence]}\r\n"
|
79
|
+
message << "Session: #{session}\r\n\r\n"
|
80
|
+
end
|
81
|
+
|
82
|
+
def self.get_parameter(stream, session, options={})
|
83
|
+
message = "GET_PARAMETER #{stream} #{RTSP_VER}\r\n"
|
84
|
+
message << "CSeq: #{options[:sequence]}\r\n"
|
85
|
+
message << "Content-Type: #{options[:content_type]}\r\n"
|
86
|
+
message << "Content-Length: #{options[:content_length]}\r\n"
|
87
|
+
message << "Session: #{session}\r\n\r\n"
|
88
|
+
end
|
89
|
+
|
90
|
+
def self.set_parameter(stream, options={})
|
91
|
+
message = "SET_PARAMETER #{stream} #{RTSP_VER}\r\n"
|
92
|
+
message << "CSeq: #{options[:sequence]}\r\n"
|
93
|
+
message << "Content-Type: #{options[:content_type]}\r\n"
|
94
|
+
message << "Content-Length: #{options[:content_length]}\r\n"
|
95
|
+
end
|
96
|
+
|
97
|
+
def self.record(stream, session, options={})
|
98
|
+
message = "RECORD #{stream} #{RTSP_VER}\r\n"
|
99
|
+
message << "CSeq: #{options[:sequence]}\r\n"
|
100
|
+
message << "Session: #{session}\r\n\r\n"
|
101
|
+
message << "Conference: #{options[:conference]}\r\n\r\n"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'socket'
|
3
|
+
require 'sdp'
|
4
|
+
|
5
|
+
module RTSP
|
6
|
+
class Response
|
7
|
+
attr_reader :code
|
8
|
+
attr_reader :message
|
9
|
+
attr_reader :body
|
10
|
+
|
11
|
+
def initialize(response)
|
12
|
+
response_array = response.split "\r\n\r\n"
|
13
|
+
|
14
|
+
if response_array.empty?
|
15
|
+
response_array = response.split "\n\n"
|
16
|
+
end
|
17
|
+
|
18
|
+
head = response_array.first
|
19
|
+
body = response_array.last == head ? "" : response_array.last
|
20
|
+
parse_head(head)
|
21
|
+
@body = parse_body(body)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Reads through each line of the RTSP response and creates a
|
25
|
+
# snake-case accessor with that value set.
|
26
|
+
#
|
27
|
+
# @param [String] head
|
28
|
+
def parse_head head
|
29
|
+
lines = head.split "\r\n"
|
30
|
+
|
31
|
+
lines.each_with_index do |line, i|
|
32
|
+
if i == 0
|
33
|
+
line =~ /RTSP\/1.0 (\d\d\d) ([^\r\n]+)/
|
34
|
+
@code = $1.to_i
|
35
|
+
@message = $2
|
36
|
+
next
|
37
|
+
end
|
38
|
+
|
39
|
+
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)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def parse_body body
|
48
|
+
#response[:body] = read_nonblock(size).split("\r\n") unless @content_length == 0
|
49
|
+
if body =~ /^(\r\n|\n)/
|
50
|
+
body.gsub!(/^(\r\n|\n)/, '')
|
51
|
+
end
|
52
|
+
|
53
|
+
if @content_type == "application/sdp"
|
54
|
+
SDP.parse body
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
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
|
+
private
|
70
|
+
|
71
|
+
def create_reader(name, value)
|
72
|
+
instance_variable_set("@#{name}", value)
|
73
|
+
self.instance_eval "def #{name}; @#{name}; end"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
data/lib/rtsp.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
|
3
|
+
# This base module simply defines properties about the library. See child
|
4
|
+
# classes/modules for the meat.
|
5
|
+
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'
|
12
|
+
end
|
data/spec/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--format documentation --color
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
2
|
+
require 'rtsp/client'
|
3
|
+
|
4
|
+
describe RTSP::Client do
|
5
|
+
context "parses the URI on init" do
|
6
|
+
it "with scheme, IP, port, and path" do
|
7
|
+
@rtsp_client = RTSP::Client.new "rtsp://64.202.98.91:554/sa.sdp"
|
8
|
+
@rtsp_client.server_uri.scheme.should == "rtsp"
|
9
|
+
@rtsp_client.server_uri.host.should == "64.202.98.91"
|
10
|
+
@rtsp_client.server_uri.port.should == 554
|
11
|
+
@rtsp_client.server_uri.path.should == "/sa.sdp"
|
12
|
+
end
|
13
|
+
|
14
|
+
it "with scheme, IP, path; port defaults to 554" do
|
15
|
+
@rtsp_client = RTSP::Client.new "rtsp://64.202.98.91/sa.sdp"
|
16
|
+
@rtsp_client.server_uri.scheme.should == "rtsp"
|
17
|
+
@rtsp_client.server_uri.host.should == "64.202.98.91"
|
18
|
+
@rtsp_client.server_uri.port.should == 554
|
19
|
+
@rtsp_client.server_uri.path.should == "/sa.sdp"
|
20
|
+
end
|
21
|
+
|
22
|
+
it "with IP, path; port defaults to 554; scheme defaults to 'rtsp'" do
|
23
|
+
@rtsp_client = RTSP::Client.new "64.202.98.91/sa.sdp"
|
24
|
+
@rtsp_client.server_uri.scheme.should == "rtsp"
|
25
|
+
@rtsp_client.server_uri.host.should == "64.202.98.91"
|
26
|
+
@rtsp_client.server_uri.port.should == 554
|
27
|
+
@rtsp_client.server_uri.path.should == "/sa.sdp"
|
28
|
+
end
|
29
|
+
|
30
|
+
it "with scheme, IP, port" do
|
31
|
+
@rtsp_client = RTSP::Client.new "rtsp://64.202.98.91:554"
|
32
|
+
@rtsp_client.server_uri.scheme.should == "rtsp"
|
33
|
+
@rtsp_client.server_uri.host.should == "64.202.98.91"
|
34
|
+
@rtsp_client.server_uri.port.should == 554
|
35
|
+
@rtsp_client.server_uri.path.should == ""
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,189 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
2
|
+
require 'rtsp/response'
|
3
|
+
|
4
|
+
OPTIONS_RESPONSE = %Q{ RTSP/1.0 200 OK\r\n
|
5
|
+
CSeq: 1\r\n
|
6
|
+
Date: Fri, Jan 28 2011 01:14:42 GMT\r\n
|
7
|
+
Public: OPTIONS, DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE\r\n
|
8
|
+
}
|
9
|
+
|
10
|
+
DESCRIBE_RESPONSE = %Q{ RTSP/1.0 200 OK\r\n
|
11
|
+
Server: DSS/5.5 (Build/489.7; Platform/Linux; Release/Darwin; )\r\n
|
12
|
+
Cseq: 1\r\n
|
13
|
+
Cache-Control: no-cache\r\n
|
14
|
+
Content-length: 406\r\n
|
15
|
+
Date: Sun, 23 Jan 2011 00:36:45 GMT\r\n
|
16
|
+
Expires: Sun, 23 Jan 2011 00:36:45 GMT\r\n
|
17
|
+
Content-Type: application/sdp\r\n
|
18
|
+
x-Accept-Retransmit: our-retransmit\r\n
|
19
|
+
x-Accept-Dynamic-Rate: 1\r\n
|
20
|
+
Content-Base: rtsp://64.202.98.91:554/gs.sdp/\r\n
|
21
|
+
\r\n\r\n
|
22
|
+
v=0
|
23
|
+
o=- 545877020 467920391 IN IP4 127.0.0.1
|
24
|
+
s=Groove Salad from SomaFM [aacPlus]
|
25
|
+
i=Downtempo Ambient Groove
|
26
|
+
c=IN IP4 0.0.0.0
|
27
|
+
t=0 0
|
28
|
+
a=x-qt-text-cmt:Orban Opticodec-PC
|
29
|
+
a=x-qt-text-nam:Groove Salad from SomaFM [aacPlus]
|
30
|
+
a=x-qt-text-inf:Downtempo Ambient Groove
|
31
|
+
a=control:*
|
32
|
+
m=audio 0 RTP/AVP 96
|
33
|
+
b=AS:48
|
34
|
+
a=rtpmap:96 MP4A-LATM/44100/2
|
35
|
+
a=fmtp:96 cpresent=0;config=400027200000
|
36
|
+
a=control:trackID=1
|
37
|
+
}
|
38
|
+
|
39
|
+
SETUP_RESPONSE = %Q{ RTSP/1.0 200 OK\r\n
|
40
|
+
CSeq: 1\r\n
|
41
|
+
Date: Fri, Jan 28 2011 01:14:42 GMT\r\n
|
42
|
+
Transport: RTP/AVP;unicast;destination=10.221.222.186;source=10.221.222.235;client_port=9000-9001;server_port=6700-6701\r\n
|
43
|
+
Session: 118\r\n
|
44
|
+
}
|
45
|
+
|
46
|
+
PLAY_RESPONSE = %Q{ RTSP/1.0 200 OK\r\n
|
47
|
+
CSeq: 1\r\n
|
48
|
+
Date: Fri, Jan 28 2011 01:14:42 GMT\r\n
|
49
|
+
Range: npt=0.000-\r\n
|
50
|
+
Session: 118\r\n
|
51
|
+
RTP-Info: url=rtsp://10.221.222.235/stream1/track1;seq=17320;rtptime=400880602\r\n
|
52
|
+
}
|
53
|
+
|
54
|
+
TEARDOWN_RESPONSE = %Q{ RTSP/1.0 200 OK\r\n
|
55
|
+
CSeq: 1\r\n
|
56
|
+
Date: Fri, Jan 28 2011 01:14:47 GMT\r\n
|
57
|
+
}
|
58
|
+
|
59
|
+
describe RTSP::Response do
|
60
|
+
context "options" do
|
61
|
+
before do
|
62
|
+
@response = RTSP::Response.new OPTIONS_RESPONSE
|
63
|
+
end
|
64
|
+
|
65
|
+
it "returns a 200 code" do
|
66
|
+
@response.code.should == 200
|
67
|
+
end
|
68
|
+
|
69
|
+
it "returns 'OK' message" do
|
70
|
+
@response.message.should == 'OK'
|
71
|
+
end
|
72
|
+
|
73
|
+
it "returns the date header" do
|
74
|
+
@response.date.should == 'Fri, Jan 28 2011 01:14:42 GMT'
|
75
|
+
end
|
76
|
+
|
77
|
+
it "returns the supported methods in the Public header" do
|
78
|
+
@response.public.should == 'OPTIONS, DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE'
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
context "describe" do
|
83
|
+
before do
|
84
|
+
@response = RTSP::Response.new DESCRIBE_RESPONSE
|
85
|
+
end
|
86
|
+
|
87
|
+
it "returns a 200 code" do
|
88
|
+
@response.code.should == 200
|
89
|
+
end
|
90
|
+
|
91
|
+
it "returns 'OK' message" do
|
92
|
+
@response.message.should == "OK"
|
93
|
+
end
|
94
|
+
|
95
|
+
it "returns all header fields" do
|
96
|
+
@response.server.should == "DSS/5.5 (Build/489.7; Platform/Linux; Release/Darwin; )"
|
97
|
+
@response.cseq.should == "1"
|
98
|
+
@response.cache_control.should == "no-cache"
|
99
|
+
@response.content_length.should == "406"
|
100
|
+
@response.date.should == "Sun, 23 Jan 2011 00:36:45 GMT"
|
101
|
+
@response.expires.should == "Sun, 23 Jan 2011 00:36:45 GMT"
|
102
|
+
@response.content_type.should == "application/sdp"
|
103
|
+
@response.x_accept_retransmit.should == "our-retransmit"
|
104
|
+
@response.x_accept_dynamic_rate.should == "1"
|
105
|
+
@response.content_base.should == "rtsp://64.202.98.91:554/gs.sdp/"
|
106
|
+
end
|
107
|
+
|
108
|
+
it "body is a parsed SDP::Description" do
|
109
|
+
@response.body.should be_kind_of SDP::Description
|
110
|
+
sdp_info = @response.body
|
111
|
+
sdp_info.protocol_version.should == "0"
|
112
|
+
sdp_info.name.should == "Groove Salad from SomaFM [aacPlus]"
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
context "setup" do
|
117
|
+
before do
|
118
|
+
@response = RTSP::Response.new SETUP_RESPONSE
|
119
|
+
end
|
120
|
+
|
121
|
+
it "returns a 200 code" do
|
122
|
+
@response.code.should == 200
|
123
|
+
end
|
124
|
+
|
125
|
+
it "returns 'OK' message" do
|
126
|
+
@response.message.should == 'OK'
|
127
|
+
end
|
128
|
+
|
129
|
+
it "returns the date header" do
|
130
|
+
@response.date.should == 'Fri, Jan 28 2011 01:14:42 GMT'
|
131
|
+
end
|
132
|
+
|
133
|
+
it "returns the supported transport" do
|
134
|
+
@response.transport.should == 'RTP/AVP;unicast;destination=10.221.222.186;source=10.221.222.235;client_port=9000-9001;server_port=6700-6701'
|
135
|
+
end
|
136
|
+
|
137
|
+
it "returns the session" do
|
138
|
+
@response.session.should == '118'
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
context "play" do
|
143
|
+
before do
|
144
|
+
@response = RTSP::Response.new PLAY_RESPONSE
|
145
|
+
end
|
146
|
+
|
147
|
+
it "returns a 200 code" do
|
148
|
+
@response.code.should == 200
|
149
|
+
end
|
150
|
+
|
151
|
+
it "returns 'OK' message" do
|
152
|
+
@response.message.should == 'OK'
|
153
|
+
end
|
154
|
+
|
155
|
+
it "returns the date header" do
|
156
|
+
@response.date.should == 'Fri, Jan 28 2011 01:14:42 GMT'
|
157
|
+
end
|
158
|
+
|
159
|
+
it "returns the supported range" do
|
160
|
+
@response.range.should == 'npt=0.000-'
|
161
|
+
end
|
162
|
+
|
163
|
+
it "returns the session" do
|
164
|
+
@response.session.should == '118'
|
165
|
+
end
|
166
|
+
|
167
|
+
it "returns the rtp_info" do
|
168
|
+
@response.rtp_info.should == 'url=rtsp://10.221.222.235/stream1/track1;seq=17320;rtptime=400880602'
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
context "teardown" do
|
173
|
+
before do
|
174
|
+
@response = RTSP::Response.new TEARDOWN_RESPONSE
|
175
|
+
end
|
176
|
+
|
177
|
+
it "returns a 200 code" do
|
178
|
+
@response.code.should == 200
|
179
|
+
end
|
180
|
+
|
181
|
+
it "returns 'OK' message" do
|
182
|
+
@response.message.should == 'OK'
|
183
|
+
end
|
184
|
+
|
185
|
+
it "returns the date header" do
|
186
|
+
@response.date.should == 'Fri, Jan 28 2011 01:14:47 GMT'
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|