rtsp_server 0.0.2-universal-java

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 +311 -0
  27. data/lib/rtsp/socat_streaming.rb +320 -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 +44 -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,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