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.
- data/.document +3 -0
- data/.infinity_test +1 -1
- data/.yardopts +4 -0
- data/ChangeLog.rdoc +9 -0
- data/Gemfile +15 -6
- data/Gemfile.lock +78 -40
- data/LICENSE.rdoc +20 -0
- data/PostInstall.txt +0 -3
- data/README.rdoc +85 -36
- data/Rakefile +33 -49
- data/bin/rtsp_client +129 -0
- data/features/client_changes_state.feature +58 -0
- data/features/client_requests.feature +27 -0
- data/features/control_streams_as_client.feature +26 -0
- data/features/step_definitions/client_changes_state_steps.rb +46 -0
- data/features/step_definitions/client_requests_steps.rb +74 -0
- data/features/step_definitions/control_streams_as_client_steps.rb +34 -0
- data/features/support/env.rb +31 -29
- data/features/support/hooks.rb +3 -0
- data/gemspec.yml +30 -0
- data/lib/ext/logger.rb +8 -0
- data/lib/rtsp.rb +3 -6
- data/lib/rtsp/capturer.rb +105 -0
- data/lib/rtsp/client.rb +446 -204
- data/lib/rtsp/error.rb +6 -0
- data/lib/rtsp/global.rb +63 -0
- data/lib/rtsp/helpers.rb +28 -0
- data/lib/rtsp/message.rb +270 -0
- data/lib/rtsp/response.rb +89 -29
- data/lib/rtsp/transport_parser.rb +64 -0
- data/lib/rtsp/version.rb +4 -0
- data/nsm_test.rb +26 -0
- data/rtsp.gemspec +284 -0
- data/sarix_test.rb +23 -0
- data/soma_test.rb +39 -0
- data/spec/rtsp/client_spec.rb +302 -27
- data/spec/rtsp/helpers_spec.rb +53 -0
- data/spec/rtsp/message_spec.rb +420 -0
- data/spec/rtsp/response_spec.rb +144 -58
- data/spec/rtsp/transport_parser_spec.rb +54 -0
- data/spec/rtsp_spec.rb +3 -3
- data/spec/spec_helper.rb +66 -7
- data/spec/support/fake_rtsp_server.rb +123 -0
- data/tasks/metrics.rake +27 -0
- data/tasks/roodi_config.yml +14 -0
- data/tasks/stats.rake +12 -0
- metadata +174 -183
- data/.autotest +0 -23
- data/History.txt +0 -4
- data/Manifest.txt +0 -26
- data/bin/rtsp +0 -121
- data/features/step_definitions/stream_steps.rb +0 -50
- data/features/stream.feature +0 -17
- data/features/support/common.rb +0 -1
- data/features/support/world.rb +0 -1
- data/lib/rtsp/request_messages.rb +0 -104
- data/lib/rtsp/status_code.rb +0 -7
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
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
|
-
|
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
|
-
|
9
|
-
|
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
|
-
#
|
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::
|
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
|
-
|
42
|
-
|
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
|
-
#
|
48
|
-
|
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
|
-
|
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
|
-
|
58
|
-
|
70
|
+
# @return [Fixnum] A session is only established after calling #setup;
|
71
|
+
# otherwise returns nil.
|
72
|
+
attr_reader :session
|
59
73
|
|
60
|
-
#
|
61
|
-
#
|
62
|
-
#
|
63
|
-
|
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
|
-
|
68
|
-
|
79
|
+
# @return [Struct::Connection]
|
80
|
+
attr_accessor :connection
|
69
81
|
|
70
|
-
|
71
|
-
|
72
|
-
|
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
|
-
|
75
|
-
|
87
|
+
# @return [Symbol] See {RFC section A.1.}[http://tools.ietf.org/html/rfc2326#page-76]
|
88
|
+
attr_reader :session_state
|
76
89
|
|
77
|
-
#
|
78
|
-
#
|
79
|
-
|
80
|
-
|
81
|
-
|
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
|
-
|
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
|
-
|
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
|
-
#
|
95
|
-
#
|
96
|
-
# @
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
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
|
-
@
|
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
|
-
|
126
|
-
|
127
|
-
|
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
|
-
#
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
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
|
-
|
151
|
-
|
152
|
-
|
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
|
-
|
156
|
-
|
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
|
-
|
160
|
-
|
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
|
-
#
|
165
|
-
|
166
|
-
|
167
|
-
|
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
|
-
|
171
|
-
|
172
|
-
|
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
|
-
|
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
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
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
|
-
|
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
|
-
#
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
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
|
-
|
210
|
-
|
211
|
-
|
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
|
-
|
214
|
-
|
215
|
-
|
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
|
-
|
220
|
-
|
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
|
-
|
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
|
-
|
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
|
-
#
|
231
|
-
#
|
232
|
-
#
|
233
|
-
|
234
|
-
def
|
235
|
-
|
236
|
-
|
237
|
-
|
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
|
-
|
449
|
+
"#{@content_base}#{aggregate_control.first[:value].gsub(/\*/, "")}"
|
240
450
|
end
|
241
451
|
|
242
|
-
#
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
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
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
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
|
-
|
261
|
-
|
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
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
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
|
-
|
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
|