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