rtsp_server 0.0.2-universal-java
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.
- checksums.yaml +7 -0
- data/.gemtest +0 -0
- data/ChangeLog.rdoc +74 -0
- data/Gemfile +3 -0
- data/LICENSE.rdoc +20 -0
- data/README.rdoc +152 -0
- data/Rakefile +23 -0
- data/bin/rtsp_client +133 -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 +52 -0
- data/features/step_definitions/client_requests_steps.rb +68 -0
- data/features/step_definitions/control_streams_as_client_steps.rb +34 -0
- data/features/support/env.rb +50 -0
- data/features/support/hooks.rb +3 -0
- data/lib/ext/logger.rb +8 -0
- data/lib/rtsp/client.rb +520 -0
- data/lib/rtsp/common.rb +148 -0
- 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 +272 -0
- data/lib/rtsp/request.rb +39 -0
- data/lib/rtsp/response.rb +47 -0
- data/lib/rtsp/server.rb +311 -0
- data/lib/rtsp/socat_streaming.rb +320 -0
- data/lib/rtsp/stream_server.rb +37 -0
- data/lib/rtsp/transport_parser.rb +96 -0
- data/lib/rtsp/version.rb +4 -0
- data/lib/rtsp.rb +6 -0
- data/rtsp.gemspec +44 -0
- data/spec/rtsp/client_spec.rb +326 -0
- data/spec/rtsp/helpers_spec.rb +53 -0
- data/spec/rtsp/message_spec.rb +420 -0
- data/spec/rtsp/response_spec.rb +306 -0
- data/spec/rtsp/transport_parser_spec.rb +137 -0
- data/spec/rtsp_spec.rb +27 -0
- data/spec/spec_helper.rb +88 -0
- data/spec/support/fake_rtsp_server.rb +123 -0
- data/tasks/roodi.rake +9 -0
- data/tasks/roodi_config.yaml +14 -0
- data/tasks/stats.rake +12 -0
- metadata +280 -0
data/lib/rtsp/server.rb
ADDED
@@ -0,0 +1,311 @@
|
|
1
|
+
require_relative 'request'
|
2
|
+
require_relative 'stream_server'
|
3
|
+
require_relative 'global'
|
4
|
+
require 'socket'
|
5
|
+
|
6
|
+
module RTSP
|
7
|
+
SUPPORTED_VERSION = "1.0"
|
8
|
+
|
9
|
+
# Instantiates an RTSP Server
|
10
|
+
# Streaming is performed using socat.
|
11
|
+
# All you need is the multicast source RTP host and port.
|
12
|
+
#
|
13
|
+
# require 'rtsp/server'
|
14
|
+
# server = RTSP::Server.new "10.221.222.90", 8554
|
15
|
+
#
|
16
|
+
# This is for the stream at index 1 (rtsp://10.221.222.90:8554/stream1)
|
17
|
+
# RTSP::StreamServer.instance.source_ip << "239.221.222.241"
|
18
|
+
# RTSP::StreamServer.instance.source_port << 6780
|
19
|
+
# RTSP::StreamServer.instance.fmtp << "96 packetization-mode=1..."
|
20
|
+
# RTSP::StreamServer.instance.rtp_map << "96 H264/90000"
|
21
|
+
#
|
22
|
+
# This is for the stream at index 2 (rtsp://10.221.222.90:8554/stream2)
|
23
|
+
# RTSP::StreamServer.instance.source_ip << "239.221.222.141"
|
24
|
+
# RTSP::StreamServer.instance.source_port << 6740
|
25
|
+
# RTSP::StreamServer.instance.fmtp << "96 packetization-mode=1..."
|
26
|
+
# RTSP::StreamServer.instance.rtp_map << "96 MP4/90000"
|
27
|
+
#
|
28
|
+
# Now start the server
|
29
|
+
# server.start
|
30
|
+
class Server
|
31
|
+
extend RTSP::Global
|
32
|
+
|
33
|
+
OPTIONS_LIST = %w(OPTIONS DESCRIBE SETUP TEARDOWN PLAY
|
34
|
+
PAUSE GET_PARAMETER SET_PARAMETER)
|
35
|
+
|
36
|
+
attr_accessor :options_list
|
37
|
+
attr_accessor :version
|
38
|
+
attr_accessor :session
|
39
|
+
attr_accessor :agent
|
40
|
+
|
41
|
+
# Initializes the the Stream Server.
|
42
|
+
#
|
43
|
+
# @param [Fixnum] host IP interface to bind.
|
44
|
+
# @param [Fixnum] port RTSP port.
|
45
|
+
def initialize(host, port=554)
|
46
|
+
@session = rand(99999999)
|
47
|
+
@stream_server = RTSP::StreamServer.instance
|
48
|
+
@interface_ip = host
|
49
|
+
@stream_server.interface_ip = host
|
50
|
+
@tcp_server = TCPServer.new(host, port)
|
51
|
+
@udp_server = UDPSocket.new
|
52
|
+
@udp_server.bind(host, port)
|
53
|
+
@agent = {}
|
54
|
+
end
|
55
|
+
|
56
|
+
# Starts accepting TCP connections
|
57
|
+
def start
|
58
|
+
Thread.start { udp_listen }
|
59
|
+
|
60
|
+
loop do
|
61
|
+
client = @tcp_server.accept
|
62
|
+
|
63
|
+
Thread.start do
|
64
|
+
begin
|
65
|
+
loop { break if serve(client) == -1 }
|
66
|
+
rescue EOFError
|
67
|
+
# do nothing
|
68
|
+
ensure
|
69
|
+
client.close unless @agent[client].include? "QuickTime"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Listens on the UDP socket for RTSP requests.
|
76
|
+
def udp_listen
|
77
|
+
loop do
|
78
|
+
data, sender = @udp_server.recvfrom(500)
|
79
|
+
response = process_request(data, sender[3])
|
80
|
+
@udp_server.send(response, 0, sender[3], sender[1])
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Serves a client request.
|
85
|
+
#
|
86
|
+
# @param [IO] io Request/response socket object.
|
87
|
+
def serve io
|
88
|
+
request_str = ""
|
89
|
+
count = 0
|
90
|
+
|
91
|
+
begin
|
92
|
+
request_str << io.read_nonblock(500)
|
93
|
+
rescue Errno::EAGAIN
|
94
|
+
return -1 if count > 50
|
95
|
+
count += 1
|
96
|
+
sleep 0.01
|
97
|
+
retry
|
98
|
+
end
|
99
|
+
|
100
|
+
response = process_request(request_str, io)
|
101
|
+
io.send(response, 0)
|
102
|
+
end
|
103
|
+
|
104
|
+
# Process an RTSP request
|
105
|
+
#
|
106
|
+
# @param [String] request_str RTSP request.
|
107
|
+
# @param [String] remote_address IP address of sender.
|
108
|
+
# @return [String] Response.
|
109
|
+
def process_request(request_str, io)
|
110
|
+
remote_address = io.remote_address.ip_address
|
111
|
+
/(?<action>.*) rtsp:\/\// =~ request_str
|
112
|
+
request = RTSP::Request.new(request_str, remote_address)
|
113
|
+
@agent[io] = request.user_agent
|
114
|
+
response, body = send(action.downcase.to_sym, request)
|
115
|
+
|
116
|
+
add_headers(request, response, body)
|
117
|
+
end
|
118
|
+
|
119
|
+
# Handles the options request.
|
120
|
+
#
|
121
|
+
# @param [RTSP::Request] request
|
122
|
+
# @return [Array<Array<String>>] Response headers and body.
|
123
|
+
def options(request)
|
124
|
+
RTSP::Server.log "Received OPTIONS request from #{request.remote_host}"
|
125
|
+
response = []
|
126
|
+
response << "Public: #{OPTIONS_LIST.join ','}"
|
127
|
+
response << "\r\n"
|
128
|
+
|
129
|
+
[response]
|
130
|
+
end
|
131
|
+
|
132
|
+
# Handles the describe request.
|
133
|
+
#
|
134
|
+
# @param [RTSP::Request] request
|
135
|
+
# @return [Array<Array<String>>] Response headers and body.
|
136
|
+
def describe(request)
|
137
|
+
RTSP::Server.log "Received DESCRIBE request from #{request.remote_host}"
|
138
|
+
description = @stream_server.description(request.multicast?, request.stream_index)
|
139
|
+
|
140
|
+
[[], description]
|
141
|
+
end
|
142
|
+
|
143
|
+
# Handles the announce request.
|
144
|
+
#
|
145
|
+
# @param [RTSP::Request] request
|
146
|
+
# @return [Array<Array<String>>] Response headers and body.
|
147
|
+
def announce(request)
|
148
|
+
[]
|
149
|
+
end
|
150
|
+
|
151
|
+
# Handles the setup request.
|
152
|
+
#
|
153
|
+
# @param [RTSP::Request] request
|
154
|
+
# @return [Array<Array<String>>] Response headers and body.
|
155
|
+
def setup(request)
|
156
|
+
RTSP::Server.log "Received SETUP request from #{request.remote_host}"
|
157
|
+
@session = @session.next
|
158
|
+
multicast_check = request.transport.include?('multicast')
|
159
|
+
server_port = @stream_server.setup_streamer(@session,
|
160
|
+
request.transport_url, request.stream_index, multicast_check)
|
161
|
+
response = []
|
162
|
+
transport = generate_transport(request, server_port, request.stream_index)
|
163
|
+
response << "Transport: #{transport.join}"
|
164
|
+
response << "Session: #{@session}"
|
165
|
+
response << "\r\n"
|
166
|
+
|
167
|
+
[response]
|
168
|
+
end
|
169
|
+
|
170
|
+
# Handles the play request.
|
171
|
+
#
|
172
|
+
# @param [RTSP::Request] request
|
173
|
+
# @return [Array<Array<String>>] Response headers and body.
|
174
|
+
def play(request)
|
175
|
+
RTSP::Server.log "Received PLAY request from #{request.remote_host}"
|
176
|
+
sid = request.session[:session_id]
|
177
|
+
response = []
|
178
|
+
response << "Session: #{sid}"
|
179
|
+
response << "Range: #{request.range}"
|
180
|
+
index = request.stream_index - 1
|
181
|
+
rtp_sequence, rtp_timestamp = @stream_server.parse_sequence_number(
|
182
|
+
@stream_server.source_ip[index], @stream_server.source_port[index])
|
183
|
+
@stream_server.start_streaming sid
|
184
|
+
response << "RTP-Info: url=#{request.url}/track1;" +
|
185
|
+
"seq=#{rtp_sequence + 6} ;rtptime=#{rtp_timestamp}"
|
186
|
+
response << "\r\n"
|
187
|
+
|
188
|
+
[response]
|
189
|
+
end
|
190
|
+
|
191
|
+
# Handles the get_parameter request.
|
192
|
+
#
|
193
|
+
# @param [RTSP::Request] request
|
194
|
+
# @return [Array<Array<String>>] Response headers and body.
|
195
|
+
def get_parameter(request)
|
196
|
+
RTSP::Server.log "Received GET_PARAMETER request from #{request.remote_host}"
|
197
|
+
" Pending Implementation"
|
198
|
+
|
199
|
+
[[]]
|
200
|
+
end
|
201
|
+
|
202
|
+
# Handles the set_parameter request.
|
203
|
+
#
|
204
|
+
# @param [RTSP::Request] request
|
205
|
+
# @return [Array<Array<String>>] Response headers and body.
|
206
|
+
def set_parameter(request)
|
207
|
+
RTSP::Server.log "Received SET_PARAMETER request from #{request.remote_host}"
|
208
|
+
" Pending Implementation"
|
209
|
+
|
210
|
+
[[]]
|
211
|
+
end
|
212
|
+
|
213
|
+
# Handles the redirect request.
|
214
|
+
#
|
215
|
+
# @param [RTSP::Request] request
|
216
|
+
# @return [Array<Array<String>>] Response headers and body.
|
217
|
+
def redirect(request)
|
218
|
+
RTSP::Server.log "Received REDIRECT request from #{request.remote_host}"
|
219
|
+
" Pending Implementation"
|
220
|
+
|
221
|
+
[[]]
|
222
|
+
end
|
223
|
+
|
224
|
+
# Handles the teardown request.
|
225
|
+
#
|
226
|
+
# @param [RTSP::Request] request
|
227
|
+
# @return [Array<Array<String>>] Response headers and body.
|
228
|
+
def teardown(request)
|
229
|
+
RTSP::Server.log "Received TEARDOWN request from #{request.remote_host}"
|
230
|
+
sid = request.session[:session_id]
|
231
|
+
@stream_server.stop_streaming sid
|
232
|
+
|
233
|
+
[[]]
|
234
|
+
end
|
235
|
+
|
236
|
+
# Handles a pause request.
|
237
|
+
#
|
238
|
+
# @param [RTSP::Request] request
|
239
|
+
# @return [Array<Array<String>>] Response headers and body.
|
240
|
+
def pause(request)
|
241
|
+
RTSP::Server.log "Received PAUSE request from #{request.remote_host}"
|
242
|
+
response = []
|
243
|
+
sid = request.session[:session_id]
|
244
|
+
response << "Session: #{sid}"
|
245
|
+
@stream_server.disconnect sid
|
246
|
+
|
247
|
+
[response]
|
248
|
+
end
|
249
|
+
|
250
|
+
# Adds the headers to the response.
|
251
|
+
#
|
252
|
+
# @param [RTSP::Request] request
|
253
|
+
# @param [Array<String>] response Response headers
|
254
|
+
# @param [String] body Response body
|
255
|
+
# @param [String] status Response status
|
256
|
+
# @return [Array<Array<String>>] Response headers and body.
|
257
|
+
def add_headers(request, response, body, status="200 OK")
|
258
|
+
result = []
|
259
|
+
version ||= SUPPORTED_VERSION
|
260
|
+
result << "RTSP/#{version} #{status}"
|
261
|
+
result << "CSeq: #{request.cseq}"
|
262
|
+
|
263
|
+
unless body.nil?
|
264
|
+
result << "Content-Type: #{request.accept}"
|
265
|
+
result << "Content-Base: #{request.url}/"
|
266
|
+
result << "Content-Length: #{body.size}"
|
267
|
+
end
|
268
|
+
|
269
|
+
result << "Date: #{Time.now.gmtime.strftime('%a, %b %d %Y %H:%M:%S GMT')}"
|
270
|
+
result << response.join("\r\n") unless response.nil?
|
271
|
+
result << body unless body.nil?
|
272
|
+
|
273
|
+
result.flatten.join "\r\n"
|
274
|
+
end
|
275
|
+
|
276
|
+
# Handles unsupported RTSP requests.
|
277
|
+
#
|
278
|
+
# @param [Symbol] method_name Method name to be called.
|
279
|
+
# @param [Array] args Arguments to be passed in to the method.
|
280
|
+
# @param [Proc] block A block of code to be passed to a method.
|
281
|
+
def method_missing(method_name, *args, &block)
|
282
|
+
RTSP::Server.log("Received request for #{method_name} (not implemented)", :warn)
|
283
|
+
|
284
|
+
[[], "Not Implemented"]
|
285
|
+
end
|
286
|
+
|
287
|
+
private
|
288
|
+
|
289
|
+
# Generates the transport headers for the response.
|
290
|
+
#
|
291
|
+
# @param [RTSP::Request] Request object.
|
292
|
+
# @param [Fixnum] server_port Port on which the stream_server is streaming from.
|
293
|
+
def generate_transport request, server_port, index=1
|
294
|
+
port_specifier = 'client_port'
|
295
|
+
transport = request.transport.split(port_specifier)
|
296
|
+
transport[1] = port_specifier + transport[1]
|
297
|
+
|
298
|
+
if request.transport.include?("unicast")
|
299
|
+
transport[0] << "destination=#{request.remote_host};"
|
300
|
+
transport[1] << ";server_port=#{server_port}-#{server_port+1}"
|
301
|
+
else
|
302
|
+
transport[0] << "destination=#{@stream_server.source_ip[index - 1]};"
|
303
|
+
transport[1] << ";ttl=10"
|
304
|
+
end
|
305
|
+
|
306
|
+
transport[0] << "source=#{@stream_server.interface_ip};"
|
307
|
+
|
308
|
+
transport
|
309
|
+
end
|
310
|
+
end
|
311
|
+
end
|
@@ -0,0 +1,320 @@
|
|
1
|
+
require 'sys/proctable'
|
2
|
+
require_relative 'global'
|
3
|
+
require 'os'
|
4
|
+
require 'ipaddr'
|
5
|
+
require 'rtp/packet'
|
6
|
+
|
7
|
+
module RTSP
|
8
|
+
module SocatStreaming
|
9
|
+
include RTSP::Global
|
10
|
+
|
11
|
+
RTCP_SOURCE = ["80c80006072dee6ad42c300f76c3b928377e99e5006c461ba92d8a3" +
|
12
|
+
"081ca0006072dee6a010e49583330444e2d41414a4248513600000000"]
|
13
|
+
MP4_RTP_MAP = "96 MP4V-ES/30000"
|
14
|
+
MP4_FMTP = "96 profile-level-id=5;config=000001b005000001b50900000100000" +
|
15
|
+
"0012000c888ba9860fa22c087828307"
|
16
|
+
H264_RTP_MAP = "96 H264/90000"
|
17
|
+
H264_FMTP = "96 packetization-mode=1;profile-level-id=428032;" +
|
18
|
+
"sprop-parameter-sets=Z0KAMtoAgAMEwAQAAjKAAAr8gYAAAYhMAABMS0IvfjAA" +
|
19
|
+
"ADEJgAAJiWhF78CA,aM48gA=="
|
20
|
+
|
21
|
+
# @return [Hash] Hash of session IDs and SOCAT commands.
|
22
|
+
attr_accessor :sessions
|
23
|
+
|
24
|
+
# @return [Hash] Hash of session IDs and pids.
|
25
|
+
attr_reader :pids
|
26
|
+
|
27
|
+
# @return [Hash] Hash of session IDs and RTCP threads.
|
28
|
+
attr_reader :rtcp_threads
|
29
|
+
|
30
|
+
# @return [Array<String>] IP address of the source camera.
|
31
|
+
attr_accessor :source_ip
|
32
|
+
|
33
|
+
# @return [Array<Fixnum>] Port where the source camera is streaming.
|
34
|
+
attr_accessor :source_port
|
35
|
+
|
36
|
+
# @return [String] IP address of the interface of the RTSP streamer.
|
37
|
+
attr_accessor :interface_ip
|
38
|
+
|
39
|
+
# @return [Fixnum] RTP timestamp of the source stream.
|
40
|
+
attr_accessor :rtp_timestamp
|
41
|
+
|
42
|
+
# @return [Fixnum] RTP sequence number of the source stream.
|
43
|
+
attr_accessor :rtp_sequence
|
44
|
+
|
45
|
+
# @return [String] RTCP source identifier.
|
46
|
+
attr_accessor :rtcp_source_identifier
|
47
|
+
|
48
|
+
# @return [Array<String>] Media type attributes.
|
49
|
+
attr_accessor :rtp_map
|
50
|
+
|
51
|
+
# @return [Array<String>] Media format attributes.
|
52
|
+
attr_accessor :fmtp
|
53
|
+
|
54
|
+
# Generates a RTCP source ID based on the friendly name.
|
55
|
+
# This ID is used in the RTCP communication with the client.
|
56
|
+
# The default +RTCP_SOURCE+ will be used if one is not provided.
|
57
|
+
#
|
58
|
+
# @param [String] friendly_name Name to be used in the RTCP source ID.
|
59
|
+
# @return [String] rtcp_source_id RTCP Source ID.
|
60
|
+
def generate_rtcp_source_id friendly_name
|
61
|
+
["80c80006072dee6ad42c300f76c3b928377e99e5006c461ba92d8a3081ca0006072dee6a010e" +
|
62
|
+
friendly_name.unpack("H*").first + "00000000"].pack("H*")
|
63
|
+
end
|
64
|
+
|
65
|
+
# Creates a RTP streamer using socat.
|
66
|
+
#
|
67
|
+
# @param [String] sid Session ID.
|
68
|
+
# @param [String] transport_url Destination IP:port.
|
69
|
+
# @param [Fixnum] index Stream index.
|
70
|
+
# @return [Fixnum] The port the streamer will stream on.
|
71
|
+
def setup_streamer(sid, transport_url, index=1, multicast=false)
|
72
|
+
dest_ip, dest_port = transport_url.split ":"
|
73
|
+
|
74
|
+
@rtcp_source_identifier ||= RTCP_SOURCE.pack("H*")
|
75
|
+
local_port = multicast ? @source_port[index - 1] : free_port(true)
|
76
|
+
|
77
|
+
unless multicast
|
78
|
+
@rtcp_threads[sid] = Thread.start do
|
79
|
+
s = UDPSocket.new
|
80
|
+
s.bind(@interface_ip, local_port+1)
|
81
|
+
|
82
|
+
loop do
|
83
|
+
begin
|
84
|
+
_, sender = s.recvfrom(36)
|
85
|
+
s.send(@rtcp_source_identifier, 0, sender[3], sender[1])
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
@cleaner ||= Thread.start { cleanup_defunct }
|
92
|
+
@processes ||= Sys::ProcTable.ps.map { |p| p.cmdline }
|
93
|
+
|
94
|
+
if multicast
|
95
|
+
@sessions[sid] = :multicast
|
96
|
+
else
|
97
|
+
@sessions[sid] = build_socat(dest_ip, dest_port, local_port, index)
|
98
|
+
end
|
99
|
+
|
100
|
+
local_port
|
101
|
+
end
|
102
|
+
|
103
|
+
SOCAT_OPTIONS = "rcvbuf=2500000,sndbuf=2500000,sndtimeo=0.00001,rcvtimeo=0.00001"
|
104
|
+
BLOCK_SIZE = 2000
|
105
|
+
BSD_OPTIONS = "setsockopt-int=0xffff:0x200:0x01"
|
106
|
+
|
107
|
+
# Start streaming for the requested session.
|
108
|
+
#
|
109
|
+
# @param [String] session ID.
|
110
|
+
def start_streaming sid
|
111
|
+
spawn_socat(sid, @sessions[sid]) unless @sessions[sid] == :multicast
|
112
|
+
end
|
113
|
+
|
114
|
+
# Stop streaming for the requested session.
|
115
|
+
#
|
116
|
+
# @param [String] session ID.
|
117
|
+
def stop_streaming sid
|
118
|
+
if sid.nil?
|
119
|
+
disconnect_all_streams
|
120
|
+
else
|
121
|
+
disconnect sid
|
122
|
+
@rtcp_threads[sid].kill unless rtcp_threads[sid].nil?
|
123
|
+
@rtcp_threads.delete sid
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# Returns the default stream description.
|
128
|
+
#
|
129
|
+
# @param[Boolean] multicast True if the description is for a multicast stream.
|
130
|
+
# @param [Fixnum] stream_index Index of the stream type.
|
131
|
+
def description multicast=false, stream_index=1
|
132
|
+
rtp_map = @rtp_map[stream_index - 1] || H264_RTP_MAP
|
133
|
+
fmtp = @fmtp[stream_index - 1] || H264_FMTP
|
134
|
+
|
135
|
+
<<EOF
|
136
|
+
v=0\r
|
137
|
+
o=- 1345481255966282 1 IN IP4 #{@interface_ip}\r
|
138
|
+
s=Session streamed by "Streaming Server"\r
|
139
|
+
i=stream1#{multicast ? 'm' : ''}\r
|
140
|
+
t=0 0\r
|
141
|
+
a=tool:LIVE555 Streaming Media v2007.07.09\r
|
142
|
+
a=type:broadcast\r
|
143
|
+
a=control:*\r
|
144
|
+
a=range:npt=0-\r
|
145
|
+
a=x-qt-text-nam:Session streamed by "Streaming Server"\r
|
146
|
+
a=x-qt-text-inf:stream1#{multicast ? 'm' : ''}\r
|
147
|
+
m=video #{multicast ? @source_port[stream_index - 1] : 0} RTP/AVP 96\r
|
148
|
+
c=IN IP4 #{multicast ? "#{multicast_ip(stream_index)}/10" : "0.0.0.0"}\r
|
149
|
+
a=rtpmap:#{rtp_map}\r
|
150
|
+
a=fmtp:#{fmtp}\r
|
151
|
+
a=control:track1\r
|
152
|
+
EOF
|
153
|
+
end
|
154
|
+
|
155
|
+
# Disconnects the stream matching the session ID.
|
156
|
+
#
|
157
|
+
# @param [String] sid Session ID.
|
158
|
+
def disconnect sid
|
159
|
+
pid = @pids[sid].to_i
|
160
|
+
@pids.delete(sid)
|
161
|
+
@sessions.delete(sid)
|
162
|
+
Process.kill(9, pid) if pid > 1000
|
163
|
+
rescue Errno::ESRCH
|
164
|
+
log "Tried to kill dead process: #{pid}"
|
165
|
+
end
|
166
|
+
|
167
|
+
# Parses the headers from an RTP stream.
|
168
|
+
#
|
169
|
+
# @param [String] src_ip Multicast IP address of RTP stream.
|
170
|
+
# @param [Fixnum] src_port Port of RTP stream.
|
171
|
+
# @return [Array<Fixnum>] Sequence number and timestamp
|
172
|
+
def parse_sequence_number(src_ip, src_port)
|
173
|
+
sock = UDPSocket.new
|
174
|
+
ip = IPAddr.new(src_ip).hton + IPAddr.new("0.0.0.0").hton
|
175
|
+
sock.setsockopt(Socket::IPPROTO_IP, Socket::IP_ADD_MEMBERSHIP, ip)
|
176
|
+
sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, 1)
|
177
|
+
sock.bind(Socket::INADDR_ANY, src_port)
|
178
|
+
|
179
|
+
begin
|
180
|
+
data = sock.recv_nonblock(1500)
|
181
|
+
rescue Errno::EAGAIN
|
182
|
+
retry
|
183
|
+
end
|
184
|
+
|
185
|
+
sock.close
|
186
|
+
packet = RTP::Packet.read(data)
|
187
|
+
|
188
|
+
[packet["sequence_number"], packet["timestamp"]]
|
189
|
+
end
|
190
|
+
|
191
|
+
private
|
192
|
+
|
193
|
+
# Returns the multicast IP on which the streamer will stream.
|
194
|
+
#
|
195
|
+
# @param [Fixnum] index Stream index.
|
196
|
+
# @return [String] Multicast IP.
|
197
|
+
def multicast_ip index=1
|
198
|
+
@interface_ip ||= find_best_interface_ipaddr @source_ip[index-1]
|
199
|
+
multicast_ip = @interface_ip.split "."
|
200
|
+
multicast_ip[0] = "239"
|
201
|
+
|
202
|
+
multicast_ip.join "."
|
203
|
+
end
|
204
|
+
|
205
|
+
# Cleans up defunct child processes
|
206
|
+
def cleanup_defunct
|
207
|
+
loop do
|
208
|
+
begin
|
209
|
+
Process.wait 0
|
210
|
+
rescue Errno::ECHILD
|
211
|
+
sleep 10
|
212
|
+
retry
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
# Determine the interface address that best matches an IP address. This
|
218
|
+
# is most useful when talking to a remote computer and needing to
|
219
|
+
# determine the interface that is being used for the connection.
|
220
|
+
#
|
221
|
+
# @param [String] device_ip IP address of the remote device you want to
|
222
|
+
# talk to.
|
223
|
+
# @return [String] IP of the interface that would be used to talk to.
|
224
|
+
def find_best_interface_ipaddr device_ip
|
225
|
+
UDPSocket.open { |s| s.connect(device_ip, 1); s.addr.last }
|
226
|
+
end
|
227
|
+
|
228
|
+
# Disconnects all streams that are currently streaming.
|
229
|
+
def disconnect_all_streams
|
230
|
+
@pids.values.each do |pid|
|
231
|
+
Process.kill(9, pid.to_i) if pid.to_i > 1000 rescue Errno::ESRCH
|
232
|
+
end
|
233
|
+
|
234
|
+
@sessions.clear
|
235
|
+
@pids.clear
|
236
|
+
end
|
237
|
+
|
238
|
+
# Spawns an instance of Socat.
|
239
|
+
#
|
240
|
+
# @param [String] sid The session ID of the stream.
|
241
|
+
# @param [String] command The SOCAT command to be spawned.
|
242
|
+
def spawn_socat(sid, command)
|
243
|
+
@processes ||= Sys::ProcTable.ps.map { |p| p.cmdline }
|
244
|
+
|
245
|
+
if command.nil?
|
246
|
+
log("SOCAT command for #{sid} was nil", :warn)
|
247
|
+
return
|
248
|
+
end
|
249
|
+
|
250
|
+
if @processes.include?(command)
|
251
|
+
pid = get_pid(command)
|
252
|
+
log "Streamer already running with pid #{pid}" if pid.is_a? Fixnum
|
253
|
+
else
|
254
|
+
@sessions[sid] = command
|
255
|
+
|
256
|
+
Thread.start do
|
257
|
+
log "Running stream spawner: #{command}"
|
258
|
+
@processes << command
|
259
|
+
pid = spawn command
|
260
|
+
@pids[sid] = pid
|
261
|
+
Thread.start { sleep 20; spawn_socat(sid, command) }
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
# Builds a socat stream command based on the source and target
|
267
|
+
# IP and ports of the RTP stream.
|
268
|
+
#
|
269
|
+
# @param [String] target_ip IP address of the remote device you want to
|
270
|
+
# talk to.
|
271
|
+
# @param [Fixnum] target_port Port on the remote device you want to
|
272
|
+
# talk to.
|
273
|
+
# @
|
274
|
+
|
275
|
+
# @return [String] IP of the interface that would be used to talk to.
|
276
|
+
def build_socat(target_ip, target_port, server_port, index=1)
|
277
|
+
bsd_options = BSD_OPTIONS if OS.mac?
|
278
|
+
bsd_options ||= ""
|
279
|
+
|
280
|
+
"socat -b #{BLOCK_SIZE} UDP-RECV:#{@source_port[index-1]},reuseaddr," +
|
281
|
+
"#{bsd_options}"+ SOCAT_OPTIONS + ",ip-add-membership=#{@source_ip[index-1]}:" +
|
282
|
+
"#{@interface_ip} UDP:#{target_ip}:#{target_port},sourceport=#{server_port}," +
|
283
|
+
SOCAT_OPTIONS
|
284
|
+
end
|
285
|
+
|
286
|
+
# Attempts to find a random bindable port between 50000-65500
|
287
|
+
#
|
288
|
+
# @param [Boolean] even Return a free even port number if true.
|
289
|
+
# @return [Number] A random bindable port between 50000-65500
|
290
|
+
# @raise [RuntimeError] When unable to locate port after 1000 attempts.
|
291
|
+
def free_port(even=false)
|
292
|
+
1000.times do
|
293
|
+
begin
|
294
|
+
port = rand(15500) + 50001
|
295
|
+
port += 1 if port % 2 != 0 && even
|
296
|
+
socket = UDPSocket.new
|
297
|
+
socket.bind('', port)
|
298
|
+
return port
|
299
|
+
rescue
|
300
|
+
# Do nothing if bind fails; continue looping
|
301
|
+
ensure
|
302
|
+
socket.close
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
raise "Unable to locate free port after 1000 attempts."
|
307
|
+
end
|
308
|
+
|
309
|
+
|
310
|
+
# Gets the pid for a SOCAT command.
|
311
|
+
#
|
312
|
+
# @param [String] cmd SOCAT command
|
313
|
+
# @return [Fixnum] PID of the process.
|
314
|
+
def get_pid cmd
|
315
|
+
Sys::ProcTable.ps.each do |p|
|
316
|
+
return p.pid.to_i if p.cmdline.include? cmd
|
317
|
+
end
|
318
|
+
end
|
319
|
+
end
|
320
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require_relative 'socat_streaming'
|
2
|
+
require 'singleton'
|
3
|
+
|
4
|
+
module RTSP
|
5
|
+
class StreamServer
|
6
|
+
include Singleton
|
7
|
+
include SocatStreaming
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@stream_module = SocatStreaming
|
11
|
+
@sessions = {}
|
12
|
+
@pids = {}
|
13
|
+
@rtcp_threads = {}
|
14
|
+
@rtp_timestamp = 2612015746
|
15
|
+
@rtp_sequence = 21934
|
16
|
+
@rtp_map = []
|
17
|
+
@fmtp = []
|
18
|
+
@source_ip = []
|
19
|
+
@source_port = []
|
20
|
+
end
|
21
|
+
|
22
|
+
# Sets the stream module to be used by the stream server.
|
23
|
+
#
|
24
|
+
# @param [Module] Module name.
|
25
|
+
def stream_module= module_name
|
26
|
+
@stream_module = module_name
|
27
|
+
self.class.send(:include, module_name)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Gets the current stream_module
|
31
|
+
#
|
32
|
+
# @return [Module] Module name.
|
33
|
+
def stream_module
|
34
|
+
@stream_module
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|