rtsp_server 0.0.1

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.
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