rtsp_server 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/.gemtest +0 -0
  3. data/ChangeLog.rdoc +74 -0
  4. data/Gemfile +3 -0
  5. data/LICENSE.rdoc +20 -0
  6. data/README.rdoc +152 -0
  7. data/Rakefile +23 -0
  8. data/bin/rtsp_client +133 -0
  9. data/features/client_changes_state.feature +58 -0
  10. data/features/client_requests.feature +27 -0
  11. data/features/control_streams_as_client.feature +26 -0
  12. data/features/step_definitions/client_changes_state_steps.rb +52 -0
  13. data/features/step_definitions/client_requests_steps.rb +68 -0
  14. data/features/step_definitions/control_streams_as_client_steps.rb +34 -0
  15. data/features/support/env.rb +50 -0
  16. data/features/support/hooks.rb +3 -0
  17. data/lib/ext/logger.rb +8 -0
  18. data/lib/rtsp/client.rb +520 -0
  19. data/lib/rtsp/common.rb +148 -0
  20. data/lib/rtsp/error.rb +6 -0
  21. data/lib/rtsp/global.rb +63 -0
  22. data/lib/rtsp/helpers.rb +28 -0
  23. data/lib/rtsp/message.rb +272 -0
  24. data/lib/rtsp/request.rb +39 -0
  25. data/lib/rtsp/response.rb +47 -0
  26. data/lib/rtsp/server.rb +303 -0
  27. data/lib/rtsp/socat_streaming.rb +309 -0
  28. data/lib/rtsp/stream_server.rb +37 -0
  29. data/lib/rtsp/transport_parser.rb +96 -0
  30. data/lib/rtsp/version.rb +4 -0
  31. data/lib/rtsp.rb +6 -0
  32. data/rtsp.gemspec +42 -0
  33. data/spec/rtsp/client_spec.rb +326 -0
  34. data/spec/rtsp/helpers_spec.rb +53 -0
  35. data/spec/rtsp/message_spec.rb +420 -0
  36. data/spec/rtsp/response_spec.rb +306 -0
  37. data/spec/rtsp/transport_parser_spec.rb +137 -0
  38. data/spec/rtsp_spec.rb +27 -0
  39. data/spec/spec_helper.rb +88 -0
  40. data/spec/support/fake_rtsp_server.rb +123 -0
  41. data/tasks/roodi.rake +9 -0
  42. data/tasks/roodi_config.yaml +14 -0
  43. data/tasks/stats.rake +12 -0
  44. metadata +280 -0
@@ -0,0 +1,303 @@
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
+ server_port = @stream_server.setup_streamer(@session,
159
+ request.transport_url, request.stream_index)
160
+ response = []
161
+ transport = generate_transport(request, server_port)
162
+ response << "Transport: #{transport.join}"
163
+ response << "Session: #{@session}"
164
+ response << "\r\n"
165
+
166
+ [response]
167
+ end
168
+
169
+ # Handles the play request.
170
+ #
171
+ # @param [RTSP::Request] request
172
+ # @return [Array<Array<String>>] Response headers and body.
173
+ def play(request)
174
+ RTSP::Server.log "Received PLAY request from #{request.remote_host}"
175
+ sid = request.session[:session_id]
176
+ response = []
177
+ response << "Session: #{sid}"
178
+ response << "Range: #{request.range}"
179
+ index = request.stream_index - 1
180
+ rtp_sequence, rtp_timestamp = @stream_server.parse_sequence_number(
181
+ @stream_server.source_ip[index], @stream_server.source_port[index])
182
+ @stream_server.start_streaming sid
183
+ response << "RTP-Info: url=#{request.url}/track1;" +
184
+ "seq=#{rtp_sequence + 6} ;rtptime=#{rtp_timestamp}"
185
+ response << "\r\n"
186
+
187
+ [response]
188
+ end
189
+
190
+ # Handles the get_parameter request.
191
+ #
192
+ # @param [RTSP::Request] request
193
+ # @return [Array<Array<String>>] Response headers and body.
194
+ def get_parameter(request)
195
+ RTSP::Server.log "Received GET_PARAMETER request from #{request.remote_host}"
196
+ " Pending Implementation"
197
+
198
+ [[]]
199
+ end
200
+
201
+ # Handles the set_parameter request.
202
+ #
203
+ # @param [RTSP::Request] request
204
+ # @return [Array<Array<String>>] Response headers and body.
205
+ def set_parameter(request)
206
+ RTSP::Server.log "Received SET_PARAMETER request from #{request.remote_host}"
207
+ " Pending Implementation"
208
+
209
+ [[]]
210
+ end
211
+
212
+ # Handles the redirect request.
213
+ #
214
+ # @param [RTSP::Request] request
215
+ # @return [Array<Array<String>>] Response headers and body.
216
+ def redirect(request)
217
+ RTSP::Server.log "Received REDIRECT request from #{request.remote_host}"
218
+ " Pending Implementation"
219
+
220
+ [[]]
221
+ end
222
+
223
+ # Handles the teardown request.
224
+ #
225
+ # @param [RTSP::Request] request
226
+ # @return [Array<Array<String>>] Response headers and body.
227
+ def teardown(request)
228
+ RTSP::Server.log "Received TEARDOWN request from #{request.remote_host}"
229
+ sid = request.session[:session_id]
230
+ @stream_server.stop_streaming sid
231
+
232
+ [[]]
233
+ end
234
+
235
+ # Handles a pause request.
236
+ #
237
+ # @param [RTSP::Request] request
238
+ # @return [Array<Array<String>>] Response headers and body.
239
+ def pause(request)
240
+ RTSP::Server.log "Received PAUSE request from #{request.remote_host}"
241
+ response = []
242
+ sid = request.session[:session_id]
243
+ response << "Session: #{sid}"
244
+ @stream_server.disconnect sid
245
+
246
+ [response]
247
+ end
248
+
249
+ # Adds the headers to the response.
250
+ #
251
+ # @param [RTSP::Request] request
252
+ # @param [Array<String>] response Response headers
253
+ # @param [String] body Response body
254
+ # @param [String] status Response status
255
+ # @return [Array<Array<String>>] Response headers and body.
256
+ def add_headers(request, response, body, status="200 OK")
257
+ result = []
258
+ version ||= SUPPORTED_VERSION
259
+ result << "RTSP/#{version} #{status}"
260
+ result << "CSeq: #{request.cseq}"
261
+
262
+ unless body.nil?
263
+ result << "Content-Type: #{request.accept}"
264
+ result << "Content-Base: #{request.url}/"
265
+ result << "Content-Length: #{body.size}"
266
+ end
267
+
268
+ result << "Date: #{Time.now.gmtime.strftime('%a, %b %d %Y %H:%M:%S GMT')}"
269
+ result << response.join("\r\n") unless response.nil?
270
+ result << body unless body.nil?
271
+
272
+ result.flatten.join "\r\n"
273
+ end
274
+
275
+ # Handles unsupported RTSP requests.
276
+ #
277
+ # @param [Symbol] method_name Method name to be called.
278
+ # @param [Array] args Arguments to be passed in to the method.
279
+ # @param [Proc] block A block of code to be passed to a method.
280
+ def method_missing(method_name, *args, &block)
281
+ RTSP::Server.log("Received request for #{method_name} (not implemented)", :warn)
282
+
283
+ [[], "Not Implemented"]
284
+ end
285
+
286
+ private
287
+
288
+ # Generates the transport headers for the response.
289
+ #
290
+ # @param [RTSP::Request] Request object.
291
+ # @param [Fixnum] server_port Port on which the stream_server is streaming from.
292
+ def generate_transport request, server_port
293
+ port_specifier = request.transport.include?("unicast") ? "client_port" : "port"
294
+ transport = request.transport.split(port_specifier)
295
+ transport[0] << "destination=#{request.remote_host};"
296
+ transport[0] << "source=#{@stream_server.interface_ip};"
297
+ transport[1] = port_specifier + transport[1]
298
+ transport[1] << ";server_port=#{server_port}-#{server_port+1}"
299
+
300
+ transport
301
+ end
302
+ end
303
+ end
@@ -0,0 +1,309 @@
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)
72
+ dest_ip, dest_port = transport_url.split ":"
73
+ @rtcp_source_identifier ||= RTCP_SOURCE.pack("H*")
74
+ local_port = free_port(true)
75
+
76
+ @rtcp_threads[sid] = Thread.start do
77
+ s = UDPSocket.new
78
+ s.bind(@interface_ip, local_port+1)
79
+
80
+ loop do
81
+ begin
82
+ _, sender = s.recvfrom(36)
83
+ s.send(@rtcp_source_identifier, 0, sender[3], sender[1])
84
+ end
85
+ end
86
+ end
87
+
88
+ @cleaner ||= Thread.start { cleanup_defunct }
89
+ @processes ||= Sys::ProcTable.ps.map { |p| p.cmdline }
90
+ @sessions[sid] = build_socat(dest_ip, dest_port, local_port, index)
91
+
92
+ local_port
93
+ end
94
+
95
+ SOCAT_OPTIONS = "rcvbuf=2500000,sndbuf=2500000,sndtimeo=0.00001,rcvtimeo=0.00001"
96
+ BLOCK_SIZE = 2000
97
+ BSD_OPTIONS = "setsockopt-int=0xffff:0x200:0x01"
98
+
99
+ # Start streaming for the requested session.
100
+ #
101
+ # @param [String] session ID.
102
+ def start_streaming sid
103
+ spawn_socat(sid, @sessions[sid])
104
+ end
105
+
106
+ # Stop streaming for the requested session.
107
+ #
108
+ # @param [String] session ID.
109
+ def stop_streaming sid
110
+ if sid.nil?
111
+ disconnect_all_streams
112
+ else
113
+ disconnect sid
114
+ @rtcp_threads[sid].kill unless rtcp_threads[sid].nil?
115
+ @rtcp_threads.delete sid
116
+ end
117
+ end
118
+
119
+ # Returns the default stream description.
120
+ #
121
+ # @param[Boolean] multicast True if the description is for a multicast stream.
122
+ # @param [Fixnum] stream_index Index of the stream type.
123
+ def description multicast=false, stream_index=1
124
+ rtp_map = @rtp_map[stream_index - 1] || H264_RTP_MAP
125
+ fmtp = @fmtp[stream_index - 1] || H264_FMTP
126
+
127
+ <<EOF
128
+ v=0\r
129
+ o=- 1345481255966282 1 IN IP4 #{@interface_ip}\r
130
+ s=Session streamed by "Streaming Server"\r
131
+ i=stream1\r
132
+ t=0 0\r
133
+ a=tool:LIVE555 Streaming Media v2007.07.09\r
134
+ a=type:broadcast\r
135
+ a=control:*\r
136
+ a=range:npt=0-\r
137
+ a=x-qt-text-nam:Session streamed by "Streaming Server"\r
138
+ a=x-qt-text-inf:stream1\r
139
+ m=video 0 RTP/AVP 96\r
140
+ c=IN IP4 #{multicast ? "#{multicast_ip(stream_index)}/10" : "0.0.0.0"}\r
141
+ a=rtpmap:#{rtp_map}\r
142
+ a=fmtp:#{fmtp}\r
143
+ a=control:track1\r
144
+ EOF
145
+ end
146
+
147
+ # Disconnects the stream matching the session ID.
148
+ #
149
+ # @param [String] sid Session ID.
150
+ def disconnect sid
151
+ pid = @pids[sid].to_i
152
+ @pids.delete(sid)
153
+ @sessions.delete(sid)
154
+ Process.kill(9, pid) if pid > 1000
155
+ rescue Errno::ESRCH
156
+ log "Tried to kill dead process: #{pid}"
157
+ end
158
+
159
+ # Parses the headers from an RTP stream.
160
+ #
161
+ # @param [String] src_ip Multicast IP address of RTP stream.
162
+ # @param [Fixnum] src_port Port of RTP stream.
163
+ # @return [Array<Fixnum>] Sequence number and timestamp
164
+ def parse_sequence_number(src_ip, src_port)
165
+ sock = UDPSocket.new
166
+ ip = IPAddr.new(src_ip).hton + IPAddr.new("0.0.0.0").hton
167
+ sock.setsockopt(Socket::IPPROTO_IP, Socket::IP_ADD_MEMBERSHIP, ip)
168
+ sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, 1)
169
+ sock.bind(Socket::INADDR_ANY, src_port)
170
+
171
+ begin
172
+ data = sock.recv_nonblock(1500)
173
+ rescue Errno::EAGAIN
174
+ retry
175
+ end
176
+
177
+ sock.close
178
+ packet = RTP::Packet.read(data)
179
+
180
+ [packet["sequence_number"], packet["timestamp"]]
181
+ end
182
+
183
+ private
184
+
185
+ # Returns the multicast IP on which the streamer will stream.
186
+ #
187
+ # @param [Fixnum] index Stream index.
188
+ # @return [String] Multicast IP.
189
+ def multicast_ip index=1
190
+ @interface_ip ||= find_best_interface_ipaddr @source_ip[index-1]
191
+ multicast_ip = @interface_ip.split "."
192
+ multicast_ip[0] = "239"
193
+
194
+ multicast_ip.join "."
195
+ end
196
+
197
+ # Cleans up defunct child processes
198
+ def cleanup_defunct
199
+ loop do
200
+ begin
201
+ Process.wait 0
202
+ rescue Errno::ECHILD
203
+ sleep 10
204
+ retry
205
+ end
206
+ end
207
+ end
208
+
209
+ # Determine the interface address that best matches an IP address. This
210
+ # is most useful when talking to a remote computer and needing to
211
+ # determine the interface that is being used for the connection.
212
+ #
213
+ # @param [String] device_ip IP address of the remote device you want to
214
+ # talk to.
215
+ # @return [String] IP of the interface that would be used to talk to.
216
+ def find_best_interface_ipaddr device_ip
217
+ UDPSocket.open { |s| s.connect(device_ip, 1); s.addr.last }
218
+ end
219
+
220
+ # Disconnects all streams that are currently streaming.
221
+ def disconnect_all_streams
222
+ @pids.values.each { |pid| Process.kill(9, pid.to_i) if pid.to_i > 1000 }
223
+ @sessions.clear
224
+ @pids.clear
225
+ end
226
+
227
+ # Spawns an instance of Socat.
228
+ #
229
+ # @param [String] sid The session ID of the stream.
230
+ # @param [String] command The SOCAT command to be spawned.
231
+ def spawn_socat(sid, command)
232
+ @processes ||= Sys::ProcTable.ps.map { |p| p.cmdline }
233
+
234
+ if command.nil?
235
+ log("SOCAT command for #{sid} was nil", :warn)
236
+ return
237
+ end
238
+
239
+ if @processes.include?(command)
240
+ pid = get_pid(command)
241
+ log "Streamer already running with pid #{pid}" if pid.is_a? Fixnum
242
+ else
243
+ @sessions[sid] = command
244
+
245
+ Thread.start do
246
+ log "Running stream spawner: #{command}"
247
+ @processes << command
248
+ pid = spawn command
249
+ @pids[sid] = pid
250
+ Thread.start { sleep 20; spawn_socat(sid, command) }
251
+ end
252
+ end
253
+ end
254
+
255
+ # Builds a socat stream command based on the source and target
256
+ # IP and ports of the RTP stream.
257
+ #
258
+ # @param [String] target_ip IP address of the remote device you want to
259
+ # talk to.
260
+ # @param [Fixnum] target_port Port on the remote device you want to
261
+ # talk to.
262
+ # @
263
+
264
+ # @return [String] IP of the interface that would be used to talk to.
265
+ def build_socat(target_ip, target_port, server_port, index=1)
266
+ bsd_options = BSD_OPTIONS if OS.mac?
267
+ bsd_options ||= ""
268
+
269
+ "socat -b #{BLOCK_SIZE} UDP-RECV:#{@source_port[index-1]},reuseaddr," +
270
+ "#{bsd_options}"+ SOCAT_OPTIONS + ",ip-add-membership=#{@source_ip[index-1]}:" +
271
+ "#{@interface_ip} UDP:#{target_ip}:#{target_port},sourceport=#{server_port}," +
272
+ SOCAT_OPTIONS
273
+ end
274
+
275
+ # Attempts to find a random bindable port between 50000-65500
276
+ #
277
+ # @param [Boolean] even Return a free even port number if true.
278
+ # @return [Number] A random bindable port between 50000-65500
279
+ # @raise [RuntimeError] When unable to locate port after 1000 attempts.
280
+ def free_port(even=false)
281
+ 1000.times do
282
+ begin
283
+ port = rand(15500) + 50001
284
+ port += 1 if port % 2 != 0 && even
285
+ socket = UDPSocket.new
286
+ socket.bind('', port)
287
+ return port
288
+ rescue
289
+ # Do nothing if bind fails; continue looping
290
+ ensure
291
+ socket.close
292
+ end
293
+ end
294
+
295
+ raise "Unable to locate free port after 1000 attempts."
296
+ end
297
+
298
+
299
+ # Gets the pid for a SOCAT command.
300
+ #
301
+ # @param [String] cmd SOCAT command
302
+ # @return [Fixnum] PID of the process.
303
+ def get_pid cmd
304
+ Sys::ProcTable.ps.each do |p|
305
+ return p.pid.to_i if p.cmdline.include? cmd
306
+ end
307
+ end
308
+ end
309
+ 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